text: # 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)
  