# Copyright (c) 2014 X-IO.
# 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.

from lxml import etree
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import base64
from oslo_service import loopingcall
from six.moves import urllib

from cinder import context
from cinder import exception
from cinder.i18n import _LE, _LI, _LW
from cinder import interface
from cinder.volume import driver
from cinder.volume.drivers.san import san
from cinder.volume import qos_specs
from cinder.volume import volume_types
from cinder.zonemanager import utils as fczm_utils

XIO_OPTS = [
    cfg.IntOpt('ise_storage_pool', default=1,
               help='Default storage pool for volumes.'),
    cfg.IntOpt('ise_raid', default=1,
               help='Raid level for ISE volumes.'),
    cfg.IntOpt('ise_connection_retries', default=5,
               help='Number of retries (per port) when establishing '
               'connection to ISE management port.'),
    cfg.IntOpt('ise_retry_interval', default=1,
               help='Interval (secs) between retries.'),
    cfg.IntOpt('ise_completion_retries', default=30,
               help='Number on retries to get completion status after '
               'issuing a command to ISE.'),
]


CONF = cfg.CONF
CONF.register_opts(XIO_OPTS)

LOG = logging.getLogger(__name__)

OPERATIONAL_STATUS = 'OPERATIONAL'
PREPARED_STATUS = 'PREPARED'
INVALID_STATUS = 'VALID'
NOTFOUND_STATUS = 'NOT FOUND'


# Raise exception for X-IO driver
def RaiseXIODriverException():
    raise exception.XIODriverException()


class XIOISEDriver(driver.VolumeDriver):

    VERSION = '1.1.4'

    # Version   Changes
    # 1.0.0     Base driver
    # 1.1.0     QoS, affinity, retype and thin support
    # 1.1.1     Fix retry loop (Bug 1429283)
    # 1.1.2     Fix host object deletion (Bug 1433450).
    # 1.1.3     Wait for volume/snapshot to be deleted.
    # 1.1.4     Force target_lun to be int (Bug 1549048)

    # ThirdPartySystems wiki page
    CI_WIKI_NAME = "X-IO_technologies_CI"

    def __init__(self, *args, **kwargs):
        super(XIOISEDriver, self).__init__()
        LOG.debug("XIOISEDriver __init__ called.")
        self.configuration = kwargs.get('configuration', None)
        self.ise_primary_ip = ''
        self.ise_secondary_ip = ''
        self.newquery = 1
        self.ise_globalid = None
        self._vol_stats = {}

    def do_setup(self, context):
        LOG.debug("XIOISEDriver do_setup called.")
        self._get_ise_globalid()

    def check_for_setup_error(self):
        LOG.debug("XIOISEDriver check_for_setup_error called.")
        # The san_ip must always be set
        if self.configuration.san_ip == "":
            LOG.error(_LE("san ip must be configured!"))
            RaiseXIODriverException()
        # The san_login must always be set
        if self.configuration.san_login == "":
            LOG.error(_LE("san_login must be configured!"))
            RaiseXIODriverException()
        # The san_password must always be set
        if self.configuration.san_password == "":
            LOG.error(_LE("san_password must be configured!"))
            RaiseXIODriverException()
        return

    def _get_version(self):
        """Return driver version."""
        return self.VERSION

    def _send_query(self):
        """Do initial query to populate ISE global id."""
        body = ''
        url = '/query'
        resp = self._connect('GET', url, body)
        status = resp['status']
        if status != 200:
            # unsuccessful - this is fatal as we need the global id
            # to build REST requests.
            LOG.error(_LE("Array query failed - No response (%d)!"), status)
            RaiseXIODriverException()
        # Successfully fetched QUERY info. Parse out globalid along with
        # ipaddress for Controller 1 and Controller 2. We assign primary
        # ipaddress to use based on controller rank
        xml_tree = etree.fromstring(resp['content'])
        # first check that the ISE is running a supported FW version
        support = {}
        support['thin'] = False
        support['clones'] = False
        support['thin-clones'] = False
        self.configuration.ise_affinity = False
        self.configuration.ise_qos = False
        capabilities = xml_tree.find('capabilities')
        if capabilities is None:
            LOG.error(_LE("Array query failed. No capabilities in response!"))
            RaiseXIODriverException()
        for node in capabilities:
            if node.tag != 'capability':
                continue
            capability = node
            if capability.attrib['value'] == '49003':
                self.configuration.ise_affinity = True
            elif capability.attrib['value'] == '49004':
                self.configuration.ise_qos = True
            elif capability.attrib['value'] == '49005':
                support['thin'] = True
            elif capability.attrib['value'] == '49006':
                support['clones'] = True
            elif capability.attrib['value'] == '49007':
                support['thin-clones'] = True
        # Make sure ISE support necessary features
        if not support['clones']:
            LOG.error(_LE("ISE FW version is not compatible with OpenStack!"))
            RaiseXIODriverException()
        # set up thin provisioning support
        self.configuration.san_thin_provision = support['thin-clones']
        # Fill in global id, primary and secondary ip addresses
        globalid = xml_tree.find('globalid')
        if globalid is None:
            LOG.error(_LE("Array query failed. No global id in XML response!"))
            RaiseXIODriverException()
        self.ise_globalid = globalid.text
        controllers = xml_tree.find('controllers')
        if controllers is None:
            LOG.error(_LE("Array query failed. No controllers in response!"))
            RaiseXIODriverException()
        for node in controllers:
            if node.tag != 'controller':
                continue
            # found a controller node
            controller = node
            ipaddress = controller.find('ipaddress')
            ranktag = controller.find('rank')
            if ipaddress is None:
                continue
            # found an ipaddress tag
            # make sure rank tag is present
            if ranktag is None:
                continue
            rank = ranktag.attrib['value']
            # make sure rank value is present
            if rank is None:
                continue
            if rank == '1':
                # rank 1 means primary (xo)
                self.ise_primary_ip = ipaddress.text
                LOG.debug('Setting primary IP to: %s.',
                          self.ise_primary_ip)
            elif rank == '0':
                # rank 0 means secondary (nxo)
                self.ise_secondary_ip = ipaddress.text
                LOG.debug('Setting secondary IP to: %s.',
                          self.ise_secondary_ip)
        # clear out new query request flag on successful fetch of QUERY info.
        self.newquery = 0
        return support

    def _get_ise_globalid(self):
        """Return ISE globalid."""
        if self.ise_globalid is None or self.newquery == 1:
            # this call will populate globalid
            self._send_query()
        if self.ise_globalid is None:
            LOG.error(_LE("ISE globalid not set!"))
            RaiseXIODriverException()
        return self.ise_globalid

    def _get_ise_primary_ip(self):
        """Return Primary IP address to REST API."""
        if self.ise_primary_ip == '':
            # Primary IP is set to ISE IP passed in from cinder.conf
            self.ise_primary_ip = self.configuration.san_ip
        if self.ise_primary_ip == '':
            # No IP - fatal.
            LOG.error(_LE("Primary IP must be set!"))
            RaiseXIODriverException()
        return self.ise_primary_ip

    def _get_ise_secondary_ip(self):
        """Return Secondary IP address to REST API."""
        if self.ise_secondary_ip != '':
            return self.ise_secondary_ip

    def _get_uri_prefix(self):
        """Returns prefix in form of http(s)://1.2.3.4"""
        prefix = ''
        # figure out if http or https should be used
        if self.configuration.driver_use_ssl:
            prefix = 'https://'
        else:
            prefix = 'http://'
        # add the IP address
        prefix += self._get_ise_primary_ip()
        return prefix

    def _opener(self, method, url, body, header):
        """Wrapper to handle connection"""
        response = {}
        response['status'] = 0
        response['content'] = ''
        response['location'] = ''
        # send the request
        req = urllib.request.Request(url, body, header)
        # Override method to allow GET, PUT, POST, DELETE
        req.get_method = lambda: method
        try:
            resp = urllib.request.urlopen(req)
        except urllib.error.HTTPError as err:
            # HTTP error. Return HTTP status and content and let caller
            # handle retries.
            response['status'] = err.code
            response['content'] = err.read()
        except urllib.error.URLError as err:
            # Connection failure.  Return a status of 0 to indicate error.
            response['status'] = 0
        else:
            # Successful. Return status code, content,
            # and location header, if present.
            response['status'] = resp.getcode()
            response['content'] = resp.read()
            response['location'] = \
                resp.info().getheader('Content-Location', '')
        return response

    def _help_call_method(self, args, retry_count):
        """Helper function used for prepare clone and delete REST calls."""
        # This function calls request method and URL and checks the response.
        # Certain cases allows for retries, while success and fatal status
        # will fall out and tell parent to break out of loop.
        # initialize remaining to one less than retries
        remaining = retry_count
        resp = self._send_cmd(args['method'], args['url'], args['arglist'])
        status = resp['status']
        if (status == 400):
            reason = ''
            if 'content' in resp:
                reason = etree.fromstring(resp['content'])
                if reason is not None:
                    reason = reason.text.upper()
            if INVALID_STATUS in reason:
                # Request failed with an invalid state. This can be because
                # source volume is in a temporary unavailable state.
                LOG.debug('REST call failed with invalid state: '
                          '%(method)s - %(status)d - %(reason)s',
                          {'method': args['method'],
                           'status': status, 'reason': reason})
                # Let parent check retry eligibility based on remaining retries
                remaining -= 1
            else:
                # Fatal error. Set remaining to 0 to make caller exit loop.
                remaining = 0
        else:
            # set remaining to 0 to make caller exit loop
            # original waiter will handle the difference between success and
            # fatal error based on resp['status'].
            remaining = 0
        return (remaining, resp)

    def _help_call_opener(self, args, retry_count):
        """Helper function to call _opener."""
        # This function calls _opener func and checks the response.
        # If response is 0 it will decrement the remaining retry count.
        # On successful connection it will set remaining to 0 to signal
        # parent to break out of loop.
        remaining = retry_count
        response = self._opener(args['method'], args['url'],
                                args['body'], args['header'])
        if response['status'] != 0:
            # We are done
            remaining = 0
        else:
            # Let parent check retry eligibility based on remaining retries.
            remaining -= 1
        # Return remaining and response
        return (remaining, response)

    def _help_wait_for_status(self, args, retry_count):
        """Helper function to wait for specified volume status"""
        # This function calls _get_volume_info and checks the response.
        # If the status strings do not match the specified status it will
        # return the remaining retry count decremented by one.
        # On successful match it will set remaining to 0 to signal
        # parent to break out of loop.
        remaining = retry_count
        info = self._get_volume_info(args['name'])
        status = args['status_string']
        if (status in info['string'] or status in info['details']):
            remaining = 0
        else:
            # Let parent check retry eligibility based on remaining retries.
            remaining -= 1
        # return remaining and volume info
        return (remaining, info)

    def _wait_for_completion(self, help_func, args, retry_count):
        """Helper function to wait for completion of passed function"""
        # Helper call loop function.
        def _call_loop(loop_args):
            remaining = loop_args['retries']
            args = loop_args['args']
            LOG.debug("In call loop (%(remaining)d) %(args)s",
                      {'remaining': remaining, 'args': args})
            (remaining, response) = loop_args['func'](args, remaining)
            if remaining == 0:
                # We are done - let our caller handle response
                raise loopingcall.LoopingCallDone(response)
            loop_args['retries'] = remaining

        # Setup retries, interval and call wait function.
        loop_args = {}
        loop_args['retries'] = retry_count
        loop_args['func'] = help_func
        loop_args['args'] = args
        interval = self.configuration.ise_retry_interval
        timer = loopingcall.FixedIntervalLoopingCall(_call_loop, loop_args)
        return timer.start(interval).wait()

    def _connect(self, method, uri, body=''):
        """Set up URL and HTML and call _opener to make request"""
        url = ''
        # see if we need to add prefix
        # this call will force primary ip to be filled in as well
        prefix = self._get_uri_prefix()
        if prefix not in uri:
            url = prefix
        url += uri
        # set up headers for XML and Auth
        header = {'Content-Type': 'application/xml; charset=utf-8'}
        auth_key = ('%s:%s'
                    % (self.configuration.san_login,
                       self.configuration.san_password))
        auth_key = base64.encode_as_text(auth_key)
        header['Authorization'] = 'Basic %s' % auth_key
        # We allow 5 retries on each IP address. If connection to primary
        # fails, secondary will be tried. If connection to secondary is
        # successful, the request flag for a new QUERY will be set. The QUERY
        # will be sent on next connection attempt to figure out which
        # controller is primary in case it has changed.
        LOG.debug("Connect: %(method)s %(url)s %(body)s",
                  {'method': method, 'url': url, 'body': body})
        using_secondary = 0
        response = {}
        response['status'] = 0
        response['location'] = ''
        response['content'] = ''
        primary_ip = self._get_ise_primary_ip()
        secondary_ip = self._get_ise_secondary_ip()
        # This will first try connecting to primary IP and then secondary IP.
        args = {}
        args['method'] = method
        args['url'] = url
        args['body'] = body
        args['header'] = header
        retries = self.configuration.ise_connection_retries
        while True:
            response = self._wait_for_completion(self._help_call_opener,
                                                 args, retries)
            if response['status'] != 0:
                # Connection succeeded. Request new query on next connection
                # attempt if we used secondary ip to sort out who should be
                # primary going forward
                self.newquery = using_secondary
                return response
            # connection failed - check if we have any retries left
            if using_secondary == 0:
                # connection on primary ip failed
                # try secondary ip
                if secondary_ip is '':
                    # if secondary is not setup yet, then assert
                    # connection on primary and secondary ip failed
                    LOG.error(_LE("Connection to %s failed and no secondary!"),
                              primary_ip)
                    RaiseXIODriverException()
                # swap primary for secondary ip in URL
                url = url.replace(primary_ip, secondary_ip)
                LOG.debug('Trying secondary IP URL: %s', url)
                using_secondary = 1
                continue
            # connection failed on both IPs - break out of the loop
            break
        # connection on primary and secondary ip failed
        LOG.error(_LE("Could not connect to %(primary)s or %(secondary)s!"),
                  {'primary': primary_ip, 'secondary': secondary_ip})
        RaiseXIODriverException()

    def _param_string(self, params):
        """Turn (name, value) pairs into single param string"""
        param_str = []
        for name, value in params.items():
            if value != '':
                param_str.append("%s=%s" % (name, value))
        return '&'.join(param_str)

    def _send_cmd(self, method, url, params=None):
        """Prepare HTTP request and call _connect"""
        params = params or {}
        # Add params to appropriate field based on method
        if method in ('GET', 'PUT'):
            if params:
                url += '?' + self._param_string(params)
            body = ''
        elif method == 'POST':
            body = self._param_string(params)
        else:
            # method like 'DELETE'
            body = ''
        # ISE REST API is mostly synchronous but has some asynchronous
        # streaks. Add retries to work around design of ISE REST API that
        # does not allow certain operations to be in process concurrently.
        # This is only an issue if lots of CREATE/DELETE/SNAPSHOT/CLONE ops
        # are issued in short order.
        return self._connect(method, url, body)

    def find_target_chap(self):
        """Return target CHAP settings"""
        chap = {}
        chap['chap_user'] = ''
        chap['chap_passwd'] = ''
        url = '/storage/arrays/%s/ionetworks' % (self._get_ise_globalid())
        resp = self._send_cmd('GET', url)
        status = resp['status']
        if status != 200:
            LOG.warning(_LW("IOnetworks GET failed (%d)"), status)
            return chap
        # Got a good response. Parse out CHAP info.  First check if CHAP is
        # enabled and if so parse out username and password.
        root = etree.fromstring(resp['content'])
        for element in root.iter():
            if element.tag != 'chap':
                continue
            chapin = element.find('chapin')
            if chapin is None:
                continue
            if chapin.attrib['value'] != '1':
                continue
            # CHAP is enabled.  Store username / pw
            chap_user = chapin.find('username')
            if chap_user is not None:
                chap['chap_user'] = chap_user.text
            chap_passwd = chapin.find('password')
            if chap_passwd is not None:
                chap['chap_passwd'] = chap_passwd.text
            break
        return chap

    def find_target_iqn(self, iscsi_ip):
        """Find Target IQN string"""
        url = '/storage/arrays/%s/controllers' % (self._get_ise_globalid())
        resp = self._send_cmd('GET', url)
        status = resp['status']
        if status != 200:
            # Not good. Throw an exception.
            LOG.error(_LE("Controller GET failed (%d)"), status)
            RaiseXIODriverException()
        # Good response.  Parse out IQN that matches iscsi_ip_address
        # passed in from cinder.conf.  IQN is 'hidden' in globalid field.
        root = etree.fromstring(resp['content'])
        for element in root.iter():
            if element.tag != 'ioport':
                continue
            ipaddrs = element.find('ipaddresses')
            if ipaddrs is None:
                continue
            for ipaddr in ipaddrs.iter():
                # Look for match with iscsi_ip_address
                if ipaddr is None or ipaddr.text != iscsi_ip:
                    continue
                endpoint = element.find('endpoint')
                if endpoint is None:
                    continue
                global_id = endpoint.find('globalid')
                if global_id is None:
                    continue
                target_iqn = global_id.text
                if target_iqn != '':
                    return target_iqn
        # Did not find a matching IQN. Upsetting.
        LOG.error(_LE("Failed to get IQN!"))
        RaiseXIODriverException()

    def find_target_wwns(self):
        """Return target WWN"""
        # Let's look for WWNs
        target_wwns = []
        target = ''
        url = '/storage/arrays/%s/controllers' % (self._get_ise_globalid())
        resp = self._send_cmd('GET', url)
        status = resp['status']
        if status != 200:
            # Not good. Throw an exception.
            LOG.error(_LE("Controller GET failed (%d)"), status)
            RaiseXIODriverException()
        # Good response. Parse out globalid (WWN) of endpoint that matches
        # protocol and type (array).
        controllers = etree.fromstring(resp['content'])
        for controller in controllers.iter():
            if controller.tag != 'controller':
                continue
            fcports = controller.find('fcports')
            if fcports is None:
                continue
            for fcport in fcports:
                if fcport.tag != 'fcport':
                    continue
                wwn_tag = fcport.find('wwn')
                if wwn_tag is None:
                    continue
                target = wwn_tag.text
                target_wwns.append(target)
        return target_wwns

    def _find_target_lun(self, location):
        """Return LUN for allocation specified in location string"""
        resp = self._send_cmd('GET', location)
        status = resp['status']
        if status != 200:
            # Not good. Throw an exception.
            LOG.error(_LE("Failed to get allocation information (%d)!"),
                      status)
            RaiseXIODriverException()
        # Good response. Parse out LUN.
        xml_tree = etree.fromstring(resp['content'])
        allocation = xml_tree.find('allocation')
        if allocation is not None:
            luntag = allocation.find('lun')
            if luntag is not None:
                return luntag.text
        # Did not find LUN. Throw an exception.
        LOG.error(_LE("Failed to get LUN information!"))
        RaiseXIODriverException()

    def _get_volume_info(self, vol_name):
        """Return status of ISE volume"""
        vol_info = {}
        vol_info['value'] = ''
        vol_info['string'] = NOTFOUND_STATUS
        vol_info['details'] = ''
        vol_info['location'] = ''
        vol_info['size'] = ''
        # Attempt to collect status value, string and details. Also pick up
        # location string from response. Location is used in REST calls
        # DELETE/SNAPSHOT/CLONE.
        # We ask for specific volume, so response should only contain one
        # volume entry.
        url = '/storage/arrays/%s/volumes' % (self._get_ise_globalid())
        resp = self._send_cmd('GET', url, {'name': vol_name})
        if resp['status'] != 200:
            LOG.warning(_LW("Could not get status for %(name)s (%(status)d)."),
                        {'name': vol_name, 'status': resp['status']})
            return vol_info
        # Good response. Parse down to Volume tag in list of one.
        root = etree.fromstring(resp['content'])
        volume_node = root.find('volume')
        if volume_node is None:
            LOG.warning(_LW("No volume node in XML content."))
            return vol_info
        # Location can be found as an attribute in the volume node tag.
        vol_info['location'] = volume_node.attrib['self']
        # Find status tag
        status = volume_node.find('status')
        if status is None:
            LOG.warning(_LW("No status payload for volume %s."), vol_name)
            return vol_info
        # Fill in value and string from status tag attributes.
        vol_info['value'] = status.attrib['value']
        vol_info['string'] = status.attrib['string'].upper()
        # Detailed status has it's own list of tags.
        details = status.find('details')
        if details is not None:
            detail = details.find('detail')
            if detail is not None:
                vol_info['details'] = detail.text.upper()
        # Get volume size
        size_tag = volume_node.find('size')
        if size_tag is not None:
            vol_info['size'] = size_tag.text
        # Return value, string, details and location.
        return vol_info

    def _alloc_location(self, volume, hostname, delete=0):
        """Find location string for allocation. Also delete alloc per reqst"""
        location = ''
        url = '/storage/arrays/%s/allocations' % (self._get_ise_globalid())
        resp = self._send_cmd('GET', url, {'name': volume['name'],
                                           'hostname': hostname})
        if resp['status'] != 200:
            LOG.error(_LE("Could not GET allocation information (%d)!"),
                      resp['status'])
            RaiseXIODriverException()
        # Good response. Find the allocation based on volume name.
        allocation_tree = etree.fromstring(resp['content'])
        for allocation in allocation_tree.iter():
            if allocation.tag != 'allocation':
                continue
            # verify volume name match
            volume_tag = allocation.find('volume')
            if volume_tag is None:
                continue
            volumename_tag = volume_tag.find('volumename')
            if volumename_tag is None:
                continue
            volumename = volumename_tag.text
            if volumename != volume['name']:
                continue
            # verified volume name match
            # find endpoints list
            endpoints = allocation.find('endpoints')
            if endpoints is None:
                continue
            # Found endpoints list. Found matching host if hostname specified,
            # otherwise any host is a go.  This is used by the caller to
            # delete all allocations (presentations) to a volume.
            for endpoint in endpoints.iter():
                if hostname != '':
                    hname_tag = endpoint.find('hostname')
                    if hname_tag is None:
                        continue
                    if hname_tag.text.upper() != hostname.upper():
                        continue
                # Found hostname match. Location string is an attribute in
                # allocation tag.
                location = allocation.attrib['self']
                # Delete allocation if requested.
                if delete == 1:
                    self._send_cmd('DELETE', location)
                    location = ''
                    break
                else:
                    return location
        return location

    def _present_volume(self, volume, hostname, lun):
        """Present volume to host at specified LUN"""
        # Set up params with volume name, host name and target lun, if
        # specified.
        target_lun = lun
        params = {'volumename': volume['name'],
                  'hostname': hostname}
        # Fill in LUN if specified.
        if target_lun != '':
            params['lun'] = target_lun
        # Issue POST call to allocation.
        url = '/storage/arrays/%s/allocations' % (self._get_ise_globalid())
        resp = self._send_cmd('POST', url, params)
        status = resp['status']
        if status == 201:
            LOG.info(_LI("Volume %s presented."), volume['name'])
        elif status == 409:
            LOG.warning(_LW("Volume %(name)s already presented (%(status)d)!"),
                        {'name': volume['name'], 'status': status})
        else:
            LOG.error(_LE("Failed to present volume %(name)s (%(status)d)!"),
                      {'name': volume['name'], 'status': status})
            RaiseXIODriverException()
        # Fetch LUN. In theory the LUN should be what caller requested.
        # We try to use shortcut as location comes back in Location header.
        # Make sure shortcut of using location header worked, if not ask
        # for it explicitly.
        location = resp['location']
        if location == '':
            location = self._alloc_location(volume, hostname)
        # Find target LUN
        if location != '':
            target_lun = self._find_target_lun(location)
        # Success. Return target LUN.
        LOG.debug("Volume %(volume)s presented: %(host)s %(lun)s",
                  {'volume': volume['name'], 'host': hostname,
                   'lun': target_lun})
        return target_lun

    def find_allocations(self, hostname):
        """Find allocations for specified host"""
        alloc_cnt = 0
        url = '/storage/arrays/%s/allocations' % (self._get_ise_globalid())
        resp = self._send_cmd('GET', url, {'hostname': hostname})
        status = resp['status']
        if status != 200:
            LOG.error(_LE("Failed to get allocation information: "
                          "%(host)s (%(status)d)!"),
                      {'host': hostname, 'status': status})
            RaiseXIODriverException()
        # Good response. Count the number of allocations.
        allocation_tree = etree.fromstring(resp['content'])
        for allocation in allocation_tree.iter():
            if allocation.tag != 'allocation':
                continue
            alloc_cnt += 1
        return alloc_cnt

    def _find_host(self, endpoints):
        """Check if host entry exists on ISE based on endpoint (IQN, WWNs)"""
        # FC host might have more than one endpoint. ISCSI has only one.
        # Check if endpoints is a list, if so use first entry in list for
        # host search.
        if type(endpoints) is list:
            for endpoint in endpoints:
                ep = endpoint
                break
        else:
            ep = endpoints
        # Got single end point. Now make REST API call to fetch all hosts
        LOG.debug("find_host: Looking for host %s.", ep)
        host = {}
        host['name'] = ''
        host['type'] = ''
        host['locator'] = ''
        params = {}
        url = '/storage/arrays/%s/hosts' % (self._get_ise_globalid())
        resp = self._send_cmd('GET', url, params)
        status = resp['status']
        if resp['status'] != 200:
            LOG.error(_LE("Could not find any hosts (%s)"), status)
            RaiseXIODriverException()
        # Good response. Try to match up a host based on end point string.
        host_tree = etree.fromstring(resp['content'])
        for host_node in host_tree.iter():
            if host_node.tag != 'host':
                continue
            # Found a host tag. Check if end point matches.
            endpoints_node = host_node.find('endpoints')
            if endpoints_node is None:
                continue
            for endpoint_node in endpoints_node.iter():
                if endpoint_node.tag != 'endpoint':
                    continue
                gid = endpoint_node.find('globalid')
                if gid is None:
                    continue
                if gid.text.upper() != ep.upper():
                    continue
                # We have a match. Fill in host name, type and locator
                host['locator'] = host_node.attrib['self']
                type_tag = host_node.find('type')
                if type_tag is not None:
                    host['type'] = type_tag.text
                name_tag = host_node.find('name')
                if name_tag is not None:
                    host['name'] = name_tag.text
                break
        # This will be filled in or '' based on findings above.
        return host

    def _create_host(self, hostname, endpoints):
        """Create host entry on ISE for connector"""
        # Create endpoint list for REST call.
        endpoint_str = ''
        if type(endpoints) is list:
            ep_str = []
            ec = 0
            for endpoint in endpoints:
                if ec == 0:
                    ep_str.append("%s" % (endpoint))
                else:
                    ep_str.append("endpoint=%s" % (endpoint))
                ec += 1
            endpoint_str = '&'.join(ep_str)
        else:
            endpoint_str = endpoints
        # Log host creation.
        LOG.debug("Create host %(host)s; %(endpoint)s",
                  {'host': hostname, 'endpoint': endpoint_str})
        # Issue REST call to create host entry of OpenStack type.
        params = {'name': hostname, 'endpoint': endpoint_str,
                  'os': 'openstack'}
        url = '/storage/arrays/%s/hosts' % (self._get_ise_globalid())
        resp = self._send_cmd('POST', url, params)
        status = resp['status']
        if status != 201 and status != 409:
            LOG.error(_LE("POST for host create failed (%s)!"), status)
            RaiseXIODriverException()
        # Successfully created host entry. Return host name.
        return hostname

    def _create_clone(self, volume, clone, clone_type):
        """Create clone worker function"""
        # This function is called for both snapshot and clone
        # clone_type specifies what type is being processed
        # Creating snapshots and clones is a two step process on current ISE
        # FW. First snapshot/clone is prepared and then created.
        volume_name = ''
        if clone_type == 'snapshot':
            volume_name = volume['volume_name']
        elif clone_type == 'clone':
            volume_name = volume['name']
        args = {}
        # Make sure source volume is ready. This is another case where
        # we have to work around asynchronous behavior in ISE REST API.
        args['name'] = volume_name
        args['status_string'] = OPERATIONAL_STATUS
        retries = self.configuration.ise_completion_retries
        vol_info = self._wait_for_completion(self._help_wait_for_status,
                                             args, retries)
        if vol_info['value'] == '0':
            LOG.debug('Source volume %s ready.', volume_name)
        else:
            LOG.error(_LE("Source volume %s not ready!"), volume_name)
            RaiseXIODriverException()
        # Prepare snapshot
        # get extra_specs and qos specs from source volume
        # these functions fill in default values for entries used below
        ctxt = context.get_admin_context()
        type_id = volume['volume_type_id']
        extra_specs = self._get_extra_specs(ctxt, type_id)
        LOG.debug("Volume %(volume_name)s extra_specs %(extra_specs)s",
                  {'volume_name': volume['name'], 'extra_specs': extra_specs})
        qos = self._get_qos_specs(ctxt, type_id)
        # Wait until snapshot/clone is prepared.
        args['method'] = 'POST'
        args['url'] = vol_info['location']
        args['status'] = 202
        args['arglist'] = {'name': clone['name'],
                           'type': clone_type,
                           'affinity': extra_specs['affinity'],
                           'IOPSmin': qos['minIOPS'],
                           'IOPSmax': qos['maxIOPS'],
                           'IOPSburst': qos['burstIOPS']}
        retries = self.configuration.ise_completion_retries
        resp = self._wait_for_completion(self._help_call_method,
                                         args, retries)
        if resp['status'] != 202:
            # clone prepare failed - bummer
            LOG.error(_LE("Prepare clone failed for %s."), clone['name'])
            RaiseXIODriverException()
        # clone prepare request accepted
        # make sure not to continue until clone prepared
        args['name'] = clone['name']
        args['status_string'] = PREPARED_STATUS
        retries = self.configuration.ise_completion_retries
        clone_info = self._wait_for_completion(self._help_wait_for_status,
                                               args, retries)
        if PREPARED_STATUS in clone_info['details']:
            LOG.debug('Clone %s prepared.', clone['name'])
        else:
            LOG.error(_LE("Clone %s not in prepared state!"), clone['name'])
            RaiseXIODriverException()
        # Clone prepared, now commit the create
        resp = self._send_cmd('PUT', clone_info['location'],
                              {clone_type: 'true'})
        if resp['status'] != 201:
            LOG.error(_LE("Commit clone failed: %(name)s (%(status)d)!"),
                      {'name': clone['name'], 'status': resp['status']})
            RaiseXIODriverException()
        # Clone create request accepted. Make sure not to return until clone
        # operational.
        args['name'] = clone['name']
        args['status_string'] = OPERATIONAL_STATUS
        retries = self.configuration.ise_completion_retries
        clone_info = self._wait_for_completion(self._help_wait_for_status,
                                               args, retries)
        if OPERATIONAL_STATUS in clone_info['string']:
            LOG.info(_LI("Clone %s created."), clone['name'])
        else:
            LOG.error(_LE("Commit failed for %s!"), clone['name'])
            RaiseXIODriverException()
        return

    def _fill_in_available_capacity(self, node, pool):
        """Fill in free capacity info for pool."""
        available = node.find('available')
        if available is None:
            pool['free_capacity_gb'] = 0
            return pool
        pool['free_capacity_gb'] = int(available.get('total'))
        # Fill in separate RAID level cap
        byred = available.find('byredundancy')
        if byred is None:
            return pool
        raid = byred.find('raid-0')
        if raid is not None:
            pool['free_capacity_gb_raid_0'] = int(raid.text)
        raid = byred.find('raid-1')
        if raid is not None:
            pool['free_capacity_gb_raid_1'] = int(raid.text)
        raid = byred.find('raid-5')
        if raid is not None:
            pool['free_capacity_gb_raid_5'] = int(raid.text)
        raid = byred.find('raid-6')
        if raid is not None:
            pool['free_capacity_gb_raid_6'] = int(raid.text)
        return pool

    def _fill_in_used_capacity(self, node, pool):
        """Fill in used capacity info for pool."""
        used = node.find('used')
        if used is None:
            pool['allocated_capacity_gb'] = 0
            return pool
        pool['allocated_capacity_gb'] = int(used.get('total'))
        # Fill in separate RAID level cap
        byred = used.find('byredundancy')
        if byred is None:
            return pool
        raid = byred.find('raid-0')
        if raid is not None:
            pool['allocated_capacity_gb_raid_0'] = int(raid.text)
        raid = byred.find('raid-1')
        if raid is not None:
            pool['allocated_capacity_gb_raid_1'] = int(raid.text)
        raid = byred.find('raid-5')
        if raid is not None:
            pool['allocated_capacity_gb_raid_5'] = int(raid.text)
        raid = byred.find('raid-6')
        if raid is not None:
            pool['allocated_capacity_gb_raid_6'] = int(raid.text)
        return pool

    def _get_pools(self):
        """Return information about all pools on ISE"""
        pools = []
        pool = {}
        vol_cnt = 0
        url = '/storage/pools'
        resp = self._send_cmd('GET', url)
        status = resp['status']
        if status != 200:
            # Request failed. Return what we have, which isn't much.
            LOG.warning(_LW("Could not get pool information (%s)!"), status)
            return (pools, vol_cnt)
        # Parse out available (free) and used. Add them up to get total.
        xml_tree = etree.fromstring(resp['content'])
        for child in xml_tree:
            if child.tag != 'pool':
                continue
            # Fill in ise pool name
            tag = child.find('name')
            if tag is not None:
                pool['pool_ise_name'] = tag.text
            # Fill in globalid
            tag = child.find('globalid')
            if tag is not None:
                pool['globalid'] = tag.text
            # Fill in pool name
            tag = child.find('id')
            if tag is not None:
                pool['pool_name'] = tag.text
            # Fill in pool status
            tag = child.find('status')
            if tag is not None:
                pool['status'] = tag.attrib['string']
                details = tag.find('details')
                if details is not None:
                    detail = details.find('detail')
                    if detail is not None:
                        pool['status_details'] = detail.text
            # Fill in available capacity
            pool = self._fill_in_available_capacity(child, pool)
            # Fill in allocated capacity
            pool = self._fill_in_used_capacity(child, pool)
            # Fill in media health and type
            media = child.find('media')
            if media is not None:
                medium = media.find('medium')
                if medium is not None:
                    health = medium.find('health')
                    if health is not None:
                        pool['health'] = int(health.text)
                    tier = medium.find('tier')
                    if tier is not None:
                        pool['media'] = tier.attrib['string']
            cap = child.find('IOPSmincap')
            if cap is not None:
                pool['minIOPS_capacity'] = cap.text
            cap = child.find('IOPSmaxcap')
            if cap is not None:
                pool['maxIOPS_capacity'] = cap.text
            cap = child.find('IOPSburstcap')
            if cap is not None:
                pool['burstIOPS_capacity'] = cap.text
            pool['total_capacity_gb'] = (int(pool['free_capacity_gb'] +
                                             pool['allocated_capacity_gb']))
            pool['QoS_support'] = self.configuration.ise_qos
            pool['reserved_percentage'] = 0
            pools.append(pool)
            # count volumes
            volumes = child.find('volumes')
            if volumes is not None:
                vol_cnt += len(volumes)
        return (pools, vol_cnt)

    def _update_volume_stats(self):
        """Update storage information"""
        self._send_query()
        data = {}
        data["vendor_name"] = 'X-IO'
        data["driver_version"] = self._get_version()
        if self.configuration.volume_backend_name:
            backend_name = self.configuration.volume_backend_name
        else:
            backend_name = self.__class__.__name__
        data["volume_backend_name"] = backend_name
        data['reserved_percentage'] = 0
        # Get total and free capacity.
        (pools, vol_cnt) = self._get_pools()
        total_cap = 0
        free_cap = 0
        # fill in global capability support
        # capacity
        for pool in pools:
            total_cap += int(pool['total_capacity_gb'])
            free_cap += int(pool['free_capacity_gb'])
        data['total_capacity_gb'] = int(total_cap)
        data['free_capacity_gb'] = int(free_cap)
        # QoS
        data['QoS_support'] = self.configuration.ise_qos
        # Volume affinity
        data['affinity'] = self.configuration.ise_affinity
        # Thin provisioning
        data['thin'] = self.configuration.san_thin_provision
        data['pools'] = pools
        data['active_volumes'] = int(vol_cnt)
        return data

    def get_volume_stats(self, refresh=False):
        """Get volume stats."""
        if refresh:
            self._vol_stats = self._update_volume_stats()
        LOG.debug("ISE get_volume_stats (total, free): %(total)s, %(free)s",
                  {'total': self._vol_stats['total_capacity_gb'],
                   'free': self._vol_stats['free_capacity_gb']})
        return self._vol_stats

    def _get_extra_specs(self, ctxt, type_id):
        """Get extra specs from volume type."""
        specs = {}
        specs['affinity'] = ''
        specs['alloctype'] = ''
        specs['pool'] = self.configuration.ise_storage_pool
        specs['raid'] = self.configuration.ise_raid
        if type_id is not None:
            volume_type = volume_types.get_volume_type(ctxt, type_id)
            extra_specs = volume_type.get('extra_specs')
            # Parse out RAID, pool and affinity values
            for key, value in extra_specs.items():
                subkey = ''
                if ':' in key:
                    fields = key.split(':')
                    key = fields[0]
                    subkey = fields[1]
                if key.upper() == 'Feature'.upper():
                    if subkey.upper() == 'Raid'.upper():
                        specs['raid'] = value
                    elif subkey.upper() == 'Pool'.upper():
                        specs['pool'] = value
                elif key.upper() == 'Affinity'.upper():
                    # Only fill this in if ISE FW supports volume affinity
                    if self.configuration.ise_affinity:
                        if subkey.upper() == 'Type'.upper():
                            specs['affinity'] = value
                elif key.upper() == 'Alloc'.upper():
                    # Only fill this in if ISE FW supports thin provisioning
                    if self.configuration.san_thin_provision:
                        if subkey.upper() == 'Type'.upper():
                            specs['alloctype'] = value
        return specs

    def _get_qos_specs(self, ctxt, type_id):
        """Get QoS specs from volume type."""
        specs = {}
        specs['minIOPS'] = ''
        specs['maxIOPS'] = ''
        specs['burstIOPS'] = ''
        if type_id is not None:
            volume_type = volume_types.get_volume_type(ctxt, type_id)
            qos_specs_id = volume_type.get('qos_specs_id')
            if qos_specs_id is not None:
                kvs = qos_specs.get_qos_specs(ctxt, qos_specs_id)['specs']
            else:
                kvs = volume_type.get('extra_specs')
            # Parse out min, max and burst values
            for key, value in kvs.items():
                if ':' in key:
                    fields = key.split(':')
                    key = fields[1]
                if key.upper() == 'minIOPS'.upper():
                    specs['minIOPS'] = value
                elif key.upper() == 'maxIOPS'.upper():
                    specs['maxIOPS'] = value
                elif key.upper() == 'burstIOPS'.upper():
                    specs['burstIOPS'] = value
        return specs

    def create_volume(self, volume):
        """Create requested volume"""
        LOG.debug("X-IO create_volume called.")
        # get extra_specs and qos based on volume type
        # these functions fill in default values for entries used below
        ctxt = context.get_admin_context()
        type_id = volume['volume_type_id']
        extra_specs = self._get_extra_specs(ctxt, type_id)
        LOG.debug("Volume %(volume_name)s extra_specs %(extra_specs)s",
                  {'volume_name': volume['name'], 'extra_specs': extra_specs})
        qos = self._get_qos_specs(ctxt, type_id)
        # Make create call
        url = '/storage/arrays/%s/volumes' % (self._get_ise_globalid())
        resp = self._send_cmd('POST', url,
                              {'name': volume['name'],
                               'size': volume['size'],
                               'pool': extra_specs['pool'],
                               'redundancy': extra_specs['raid'],
                               'affinity': extra_specs['affinity'],
                               'alloctype': extra_specs['alloctype'],
                               'IOPSmin': qos['minIOPS'],
                               'IOPSmax': qos['maxIOPS'],
                               'IOPSburst': qos['burstIOPS']})
        if resp['status'] != 201:
            LOG.error(_LE("Failed to create volume: %(name)s (%(status)s)"),
                      {'name': volume['name'], 'status': resp['status']})
            RaiseXIODriverException()
        # Good response. Make sure volume is in operational state before
        # returning. Volume creation completes asynchronously.
        args = {}
        args['name'] = volume['name']
        args['status_string'] = OPERATIONAL_STATUS
        retries = self.configuration.ise_completion_retries
        vol_info = self._wait_for_completion(self._help_wait_for_status,
                                             args, retries)
        if OPERATIONAL_STATUS in vol_info['string']:
            # Ready.
            LOG.info(_LI("Volume %s created"), volume['name'])
        else:
            LOG.error(_LE("Failed to create volume %s."), volume['name'])
            RaiseXIODriverException()
        return

    def create_cloned_volume(self, volume, src_vref):
        """Create clone"""
        LOG.debug("X-IO create_cloned_volume called.")
        self._create_clone(src_vref, volume, 'clone')

    def create_snapshot(self, snapshot):
        """Create snapshot"""
        LOG.debug("X-IO create_snapshot called.")
        # Creating a snapshot uses same interface as clone operation on
        # ISE. Clone type ('snapshot' or 'clone') tells the ISE what kind
        # of operation is requested.
        self._create_clone(snapshot, snapshot, 'snapshot')

    def create_volume_from_snapshot(self, volume, snapshot):
        """Create volume from snapshot"""
        LOG.debug("X-IO create_volume_from_snapshot called.")
        # ISE snapshots are just like a volume so this is a clone operation.
        self._create_clone(snapshot, volume, 'clone')

    def _delete_volume(self, volume):
        """Delete specified volume"""
        # First unpresent volume from all hosts.
        self._alloc_location(volume, '', 1)
        # Get volume status. Location string for volume comes back
        # in response. Used for DELETE call below.
        vol_info = self._get_volume_info(volume['name'])
        if vol_info['location'] == '':
            LOG.warning(_LW("%s not found!"), volume['name'])
            return
        # Make DELETE call.
        args = {}
        args['method'] = 'DELETE'
        args['url'] = vol_info['location']
        args['arglist'] = {}
        args['status'] = 204
        retries = self.configuration.ise_completion_retries
        resp = self._wait_for_completion(self._help_call_method, args, retries)
        if resp['status'] != 204:
            LOG.warning(_LW("DELETE call failed for %s!"), volume['name'])
            return
        # DELETE call successful, now wait for completion.
        # We do that by waiting for the REST call to return Volume Not Found.
        args['method'] = ''
        args['url'] = ''
        args['name'] = volume['name']
        args['status_string'] = NOTFOUND_STATUS
        retries = self.configuration.ise_completion_retries
        vol_info = self._wait_for_completion(self._help_wait_for_status,
                                             args, retries)
        if NOTFOUND_STATUS in vol_info['string']:
            # Volume no longer present on the backend.
            LOG.info(_LI("Successfully deleted %s."), volume['name'])
            return
        # If we come here it means the volume is still present
        # on the backend.
        LOG.error(_LE("Timed out deleting %s!"), volume['name'])
        return

    def delete_volume(self, volume):
        """Delete specified volume"""
        LOG.debug("X-IO delete_volume called.")
        self._delete_volume(volume)

    def delete_snapshot(self, snapshot):
        """Delete snapshot"""
        LOG.debug("X-IO delete_snapshot called.")
        # Delete snapshot and delete volume is identical to ISE.
        self._delete_volume(snapshot)

    def _modify_volume(self, volume, new_attributes):
        # Get volume status. Location string for volume comes back
        # in response. Used for PUT call below.
        vol_info = self._get_volume_info(volume['name'])
        if vol_info['location'] == '':
            LOG.error(_LE("modify volume: %s does not exist!"), volume['name'])
            RaiseXIODriverException()
        # Make modify volume REST call using PUT.
        # Location from above is used as identifier.
        resp = self._send_cmd('PUT', vol_info['location'], new_attributes)
        status = resp['status']
        if status == 201:
            LOG.debug("Volume %s modified.", volume['name'])
            return True
        LOG.error(_LE("Modify volume PUT failed: %(name)s (%(status)d)."),
                  {'name': volume['name'], 'status': status})
        RaiseXIODriverException()

    def extend_volume(self, volume, new_size):
        """Extend volume to new size."""
        LOG.debug("extend_volume called")
        ret = self._modify_volume(volume, {'size': new_size})
        if ret is True:
            LOG.info(_LI("volume %(name)s extended to %(size)d."),
                     {'name': volume['name'], 'size': new_size})
        return

    def retype(self, ctxt, volume, new_type, diff, host):
        """Convert the volume to be of the new type."""
        LOG.debug("X-IO retype called")
        qos = self._get_qos_specs(ctxt, new_type['id'])
        ret = self._modify_volume(volume, {'IOPSmin': qos['minIOPS'],
                                           'IOPSmax': qos['maxIOPS'],
                                           'IOPSburst': qos['burstIOPS']})
        if ret is True:
            LOG.info(_LI("Volume %s retyped."), volume['name'])
        return True

    def manage_existing(self, volume, ise_volume_ref):
        """Convert an existing ISE volume to a Cinder volume."""
        LOG.debug("X-IO manage_existing called")
        if 'source-name' not in ise_volume_ref:
            LOG.error(_LE("manage_existing: No source-name in ref!"))
            RaiseXIODriverException()
        # copy the source-name to 'name' for modify volume use
        ise_volume_ref['name'] = ise_volume_ref['source-name']
        ctxt = context.get_admin_context()
        qos = self._get_qos_specs(ctxt, volume['volume_type_id'])
        ret = self._modify_volume(ise_volume_ref,
                                  {'name': volume['name'],
                                   'IOPSmin': qos['minIOPS'],
                                   'IOPSmax': qos['maxIOPS'],
                                   'IOPSburst': qos['burstIOPS']})
        if ret is True:
            LOG.info(_LI("Volume %s converted."), ise_volume_ref['name'])
        return ret

    def manage_existing_get_size(self, volume, ise_volume_ref):
        """Get size of an existing ISE volume."""
        LOG.debug("X-IO manage_existing_get_size called")
        if 'source-name' not in ise_volume_ref:
            LOG.error(_LE("manage_existing_get_size: No source-name in ref!"))
            RaiseXIODriverException()
        ref_name = ise_volume_ref['source-name']
        # get volume status including size
        vol_info = self._get_volume_info(ref_name)
        if vol_info['location'] == '':
            LOG.error(_LE("manage_existing_get_size: %s does not exist!"),
                      ref_name)
            RaiseXIODriverException()
        return int(vol_info['size'])

    def unmanage(self, volume):
        """Remove Cinder management from ISE volume"""
        LOG.debug("X-IO unmanage called")
        vol_info = self._get_volume_info(volume['name'])
        if vol_info['location'] == '':
            LOG.error(_LE("unmanage: Volume %s does not exist!"),
                      volume['name'])
            RaiseXIODriverException()
        # This is a noop. ISE does not store any Cinder specific information.

    def ise_present(self, volume, hostname_in, endpoints):
        """Set up presentation for volume and specified connector"""
        LOG.debug("X-IO ise_present called.")
        # Create host entry on ISE if necessary.
        # Check to see if host entry already exists.
        # Create if not found
        host = self._find_host(endpoints)
        if host['name'] == '':
            # host not found, so create new host entry
            # Use host name if filled in. If blank, ISE will make up a name.
            self._create_host(hostname_in, endpoints)
            host = self._find_host(endpoints)
            if host['name'] == '':
                # host still not found, this is fatal.
                LOG.error(_LE("Host could not be found!"))
                RaiseXIODriverException()
        elif host['type'].upper() != 'OPENSTACK':
            # Make sure host type is marked as OpenStack host
            params = {'os': 'openstack'}
            resp = self._send_cmd('PUT', host['locator'], params)
            status = resp['status']
            if status != 201 and status != 409:
                LOG.error(_LE("Host PUT failed (%s)."), status)
                RaiseXIODriverException()
        # We have a host object.
        target_lun = ''
        # Present volume to host.
        target_lun = self._present_volume(volume, host['name'], target_lun)
        # Fill in target information.
        data = {}
        data['target_lun'] = int(target_lun)
        data['volume_id'] = volume['id']
        return data

    def ise_unpresent(self, volume, endpoints):
        """Delete presentation between volume and connector"""
        LOG.debug("X-IO ise_unpresent called.")
        # Delete allocation uses host name. Go find it based on endpoints.
        host = self._find_host(endpoints)
        if host['name'] != '':
            # Delete allocation based on hostname and volume.
            self._alloc_location(volume, host['name'], 1)
        return host['name']

    def create_export(self, context, volume):
        LOG.debug("X-IO create_export called.")

    def ensure_export(self, context, volume):
        LOG.debug("X-IO ensure_export called.")

    def remove_export(self, context, volume):
        LOG.debug("X-IO remove_export called.")

    def local_path(self, volume):
        LOG.debug("X-IO local_path called.")

    def delete_host(self, endpoints):
        """Delete ISE host object"""
        host = self._find_host(endpoints)
        if host['locator'] != '':
            # Delete host
            self._send_cmd('DELETE', host['locator'])
            LOG.debug("X-IO: host %s deleted", host['name'])


# Protocol specific classes for entry.  They are wrappers around base class
# above and every external API resuslts in a call to common function in base
# class.
@interface.volumedriver
class XIOISEISCSIDriver(driver.ISCSIDriver):

    """Requires ISE Running FW version 3.1.0 or higher"""

    # ThirdPartySystems wiki page
    CI_WIKI_NAME = 'X-IO_technologies_CI'
    VERSION = XIOISEDriver.VERSION

    def __init__(self, *args, **kwargs):
        super(XIOISEISCSIDriver, self).__init__(*args, **kwargs)
        self.configuration.append_config_values(XIO_OPTS)
        self.configuration.append_config_values(san.san_opts)

        # The iscsi_ip_address must always be set.
        if self.configuration.iscsi_ip_address == '':
            LOG.error(_LE("iscsi_ip_address must be set!"))
            RaiseXIODriverException()
        # Setup common driver
        self.driver = XIOISEDriver(configuration=self.configuration)

    def do_setup(self, context):
        return self.driver.do_setup(context)

    def check_for_setup_error(self):
        return self.driver.check_for_setup_error()

    def local_path(self, volume):
        return self.driver.local_path(volume)

    def get_volume_stats(self, refresh=False):
        data = self.driver.get_volume_stats(refresh)
        data["storage_protocol"] = 'iSCSI'
        return data

    def create_volume(self, volume):
        self.driver.create_volume(volume)
        # Volume created successfully. Fill in CHAP information.
        model_update = {}
        chap = self.driver.find_target_chap()
        if chap['chap_user'] != '':
            model_update['provider_auth'] = 'CHAP %s %s' % \
                (chap['chap_user'], chap['chap_passwd'])
        else:
            model_update['provider_auth'] = ''
        return model_update

    def create_cloned_volume(self, volume, src_vref):
        return self.driver.create_cloned_volume(volume, src_vref)

    def create_volume_from_snapshot(self, volume, snapshot):
        return self.driver.create_volume_from_snapshot(volume, snapshot)

    def delete_volume(self, volume):
        return self.driver.delete_volume(volume)

    def extend_volume(self, volume, new_size):
        return self.driver.extend_volume(volume, new_size)

    def retype(self, ctxt, volume, new_type, diff, host):
        return self.driver.retype(ctxt, volume, new_type, diff, host)

    def manage_existing(self, volume, ise_volume_ref):
        ret = self.driver.manage_existing(volume, ise_volume_ref)
        if ret is True:
            # Volume converted successfully. Fill in CHAP information.
            model_update = {}
            chap = {}
            chap = self.driver.find_target_chap()
            if chap['chap_user'] != '':
                model_update['provider_auth'] = 'CHAP %s %s' % \
                    (chap['chap_user'], chap['chap_passwd'])
            else:
                model_update['provider_auth'] = ''
            return model_update

    def manage_existing_get_size(self, volume, ise_volume_ref):
        return self.driver.manage_existing_get_size(volume, ise_volume_ref)

    def unmanage(self, volume):
        return self.driver.unmanage(volume)

    def initialize_connection(self, volume, connector):
        hostname = ''
        if 'host' in connector:
            hostname = connector['host']
        data = self.driver.ise_present(volume, hostname,
                                       connector['initiator'])
        # find IP for target
        data['target_portal'] = \
            '%s:3260' % (self.configuration.iscsi_ip_address)
        # set IQN for target
        data['target_discovered'] = False
        data['target_iqn'] = \
            self.driver.find_target_iqn(self.configuration.iscsi_ip_address)
        # Fill in authentication method (CHAP)
        if 'provider_auth' in volume:
            auth = volume['provider_auth']
            if auth:
                (auth_method, auth_username, auth_secret) = auth.split()
                data['auth_method'] = auth_method
                data['auth_username'] = auth_username
                data['auth_password'] = auth_secret
        return {'driver_volume_type': 'iscsi',
                'data': data}

    def terminate_connection(self, volume, connector, **kwargs):
        hostname = self.driver.ise_unpresent(volume, connector['initiator'])
        alloc_cnt = 0
        if hostname != '':
            alloc_cnt = self.driver.find_allocations(hostname)
            if alloc_cnt == 0:
                # delete host object
                self.driver.delete_host(connector['initiator'])

    def create_snapshot(self, snapshot):
        return self.driver.create_snapshot(snapshot)

    def delete_snapshot(self, snapshot):
        return self.driver.delete_snapshot(snapshot)

    def create_export(self, context, volume, connector):
        return self.driver.create_export(context, volume)

    def ensure_export(self, context, volume):
        return self.driver.ensure_export(context, volume)

    def remove_export(self, context, volume):
        return self.driver.remove_export(context, volume)


@interface.volumedriver
class XIOISEFCDriver(driver.FibreChannelDriver):

    """Requires ISE Running FW version 2.8.0 or higher"""

    # ThirdPartySystems wiki page
    CI_WIKI_NAME = 'X-IO_technologies_CI'
    VERSION = XIOISEDriver.VERSION

    def __init__(self, *args, **kwargs):
        super(XIOISEFCDriver, self).__init__(*args, **kwargs)
        self.configuration.append_config_values(XIO_OPTS)
        self.configuration.append_config_values(san.san_opts)
        self.driver = XIOISEDriver(configuration=self.configuration)

    def do_setup(self, context):
        return self.driver.do_setup(context)

    def check_for_setup_error(self):
        return self.driver.check_for_setup_error()

    def local_path(self, volume):
        return self.driver.local_path(volume)

    def get_volume_stats(self, refresh=False):
        data = self.driver.get_volume_stats(refresh)
        data["storage_protocol"] = 'fibre_channel'
        return data

    def create_volume(self, volume):
        return self.driver.create_volume(volume)

    def create_cloned_volume(self, volume, src_vref):
        return self.driver.create_cloned_volume(volume, src_vref)

    def create_volume_from_snapshot(self, volume, snapshot):
        return self.driver.create_volume_from_snapshot(volume, snapshot)

    def delete_volume(self, volume):
        return self.driver.delete_volume(volume)

    def extend_volume(self, volume, new_size):
        return self.driver.extend_volume(volume, new_size)

    def retype(self, ctxt, volume, new_type, diff, host):
        return self.driver.retype(ctxt, volume, new_type, diff, host)

    def manage_existing(self, volume, ise_volume_ref):
        return self.driver.manage_existing(volume, ise_volume_ref)

    def manage_existing_get_size(self, volume, ise_volume_ref):
        return self.driver.manage_existing_get_size(volume, ise_volume_ref)

    def unmanage(self, volume):
        return self.driver.unmanage(volume)

    @fczm_utils.AddFCZone
    def initialize_connection(self, volume, connector):
        hostname = ''
        if 'host' in connector:
            hostname = connector['host']
        data = self.driver.ise_present(volume, hostname, connector['wwpns'])
        data['target_discovered'] = True
        # set wwns for target
        target_wwns = self.driver.find_target_wwns()
        data['target_wwn'] = target_wwns
        # build target initiator map
        target_map = {}
        for initiator in connector['wwpns']:
            target_map[initiator] = target_wwns
        data['initiator_target_map'] = target_map
        return {'driver_volume_type': 'fibre_channel',
                'data': data}

    @fczm_utils.RemoveFCZone
    def terminate_connection(self, volume, connector, **kwargs):
        # now we are ready to tell ISE to delete presentations
        hostname = self.driver.ise_unpresent(volume, connector['wwpns'])
        # set target_wwn and initiator_target_map only if host
        # has no more presentations
        data = {}
        alloc_cnt = 0
        if hostname != '':
            alloc_cnt = self.driver.find_allocations(hostname)
            if alloc_cnt == 0:
                target_wwns = self.driver.find_target_wwns()
                data['target_wwn'] = target_wwns
                # build target initiator map
                target_map = {}
                for initiator in connector['wwpns']:
                    target_map[initiator] = target_wwns
                data['initiator_target_map'] = target_map
                # delete host object
                self.driver.delete_host(connector['wwpns'])

        return {'driver_volume_type': 'fibre_channel',
                'data': data}

    def create_snapshot(self, snapshot):
        return self.driver.create_snapshot(snapshot)

    def delete_snapshot(self, snapshot):
        return self.driver.delete_snapshot(snapshot)

    def create_export(self, context, volume, connector):
        return self.driver.create_export(context, volume)

    def ensure_export(self, context, volume):
        return self.driver.ensure_export(context, volume)

    def remove_export(self, context, volume):
        return self.driver.remove_export(context, volume)
