text: # Copyright (c) 2019 Zadara Storage, Inc.
  # All Rights Reserved.
  #
  #    Licensed under the Apache License, Version 2.0 (the "License"); you may
  #    not use this file except in compliance with the License. You may obtain
  #    a copy of the License at
  #
  #         http://www.apache.org/licenses/LICENSE-2.0
  #
  #    Unless required by applicable law or agreed to in writing, software
  #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  #    License for the specific language governing permissions and limitations
  #    under the License.
  """Volume driver for Zadara Virtual Private Storage Array (VPSA).
  
  This driver requires VPSA with API version 15.07 or higher.
  """
  
  from lxml import etree
  from oslo_config import cfg
  from oslo_log import log as logging
  from oslo_utils import strutils
  import requests
  import six
  
  from cinder import exception
  from cinder.i18n import _
  from cinder import interface
  from cinder.volume import configuration
  from cinder.volume import driver
  
  LOG = logging.getLogger(__name__)
  
  zadara_opts = [
      cfg.BoolOpt('zadara_use_iser',
                  default=True,
                  help='VPSA - Use ISER instead of iSCSI'),
      cfg.StrOpt('zadara_vpsa_host',
                 default=None,
                 help='VPSA - Management Host name or IP address'),
      cfg.PortOpt('zadara_vpsa_port',
                  default=None,
                  help='VPSA - Port number'),
      cfg.BoolOpt('zadara_vpsa_use_ssl',
                  default=False,
                  help='VPSA - Use SSL connection'),
      cfg.BoolOpt('zadara_ssl_cert_verify',
                  default=True,
                  help='If set to True the http client will validate the SSL '
                       'certificate of the VPSA endpoint.'),
      cfg.StrOpt('zadara_user',
                 default=None,
                 deprecated_for_removal=True,
                 help='VPSA - Username'),
      cfg.StrOpt('zadara_password',
                 default=None,
                 help='VPSA - Password',
                 deprecated_for_removal=True,
                 secret=True),
      cfg.StrOpt('zadara_access_key',
                 default=None,
                 help='VPSA access key',
                 secret=True),
      cfg.StrOpt('zadara_vpsa_poolname',
                 default=None,
                 help='VPSA - Storage Pool assigned for volumes'),
      cfg.BoolOpt('zadara_vol_encrypt',
                  default=False,
                  help='VPSA - Default encryption policy for volumes'),
      cfg.StrOpt('zadara_vol_name_template',
                 default='OS_%s',
                 help='VPSA - Default template for VPSA volume names'),
      cfg.BoolOpt('zadara_default_snap_policy',
                  default=False,
                  help="VPSA - Attach snapshot policy for volumes")]
  CONF = cfg.CONF
  CONF.register_opts(zadara_opts, group=configuration.SHARED_CONF_GROUP)
  
  
  class ZadaraServerCreateFailure(exception.VolumeDriverException):
      message = _("Unable to create server object for initiator %(name)s")
  
  
  class ZadaraServerNotFound(exception.NotFound):
      message = _("Unable to find server object for initiator %(name)s")
  
  
  class ZadaraVPSANoActiveController(exception.VolumeDriverException):
      message = _("Unable to find any active VPSA controller")
  
  
  class ZadaraAttachmentsNotFound(exception.NotFound):
      message = _("Failed to retrieve attachments for volume %(name)s")
  
  
  class ZadaraInvalidAttachmentInfo(exception.Invalid):
      message = _("Invalid attachment info for volume %(name)s: %(reason)s")
  
  
  class ZadaraVolumeNotFound(exception.VolumeDriverException):
      message = "%(reason)s"
  
  
  class ZadaraInvalidAccessKey(exception.VolumeDriverException):
      message = "Invalid VPSA access key"
  
  
  class ZadaraVPSAConnection(object):
      """Executes volume driver commands on VPSA."""
  
      def __init__(self, conf):
          self.conf = conf
          self.access_key = conf.zadara_access_key
  
          self.ensure_connection()
  
      def _generate_vpsa_cmd(self, cmd, **kwargs):
          """Generate command to be sent to VPSA."""
  
          # Dictionary of applicable VPSA commands in the following format:
          # 'command': (method, API_URL, {optional parameters})
          vpsa_commands = {
              'login': ('POST',
                        '/api/users/login.xml',
                        {'user': self.conf.zadara_user,
                         'password': self.conf.zadara_password}),
              # Volume operations
              'create_volume': ('POST',
                                '/api/volumes.xml',
                                {'name': kwargs.get('name'),
                                 'capacity': kwargs.get('size'),
                                 'pool': self.conf.zadara_vpsa_poolname,
                                 'thin': 'YES',
                                 'crypt': 'YES'
                                 if self.conf.zadara_vol_encrypt else 'NO',
                                 'attachpolicies': 'NO'
                                 if not self.conf.zadara_default_snap_policy
                                 else 'YES'}),
              'delete_volume': ('DELETE',
                                '/api/volumes/%s.xml' % kwargs.get('vpsa_vol'),
                                {'force': 'YES'}),
              'expand_volume': ('POST',
                                '/api/volumes/%s/expand.xml'
                                % kwargs.get('vpsa_vol'),
                                {'capacity': kwargs.get('size')}),
              # Snapshot operations
              # Snapshot request is triggered for a single volume though the
              # API call implies that snapshot is triggered for CG (legacy API).
              'create_snapshot': ('POST',
                                  '/api/consistency_groups/%s/snapshots.xml'
                                  % kwargs.get('cg_name'),
                                  {'display_name': kwargs.get('snap_name')}),
              'delete_snapshot': ('DELETE',
                                  '/api/snapshots/%s.xml'
                                  % kwargs.get('snap_id'),
                                  {}),
              'create_clone_from_snap': ('POST',
                                         '/api/consistency_groups/%s/clone.xml'
                                         % kwargs.get('cg_name'),
                                         {'name': kwargs.get('name'),
                                          'snapshot': kwargs.get('snap_id')}),
              'create_clone': ('POST',
                               '/api/consistency_groups/%s/clone.xml'
                               % kwargs.get('cg_name'),
                               {'name': kwargs.get('name')}),
              # Server operations
              'create_server': ('POST',
                                '/api/servers.xml',
                                {'display_name': kwargs.get('initiator'),
                                 'iqn': kwargs.get('initiator')}),
              # Attach/Detach operations
              'attach_volume': ('POST',
                                '/api/servers/%s/volumes.xml'
                                % kwargs.get('vpsa_srv'),
                                {'volume_name[]': kwargs.get('vpsa_vol'),
                                 'force': 'NO'}),
              'detach_volume': ('POST',
                                '/api/volumes/%s/detach.xml'
                                % kwargs.get('vpsa_vol'),
                                {'server_name[]': kwargs.get('vpsa_srv'),
                                 'force': 'YES'}),
              # Get operations
              'list_volumes': ('GET',
                               '/api/volumes.xml',
                               {}),
              'list_pools': ('GET',
                             '/api/pools.xml',
                             {}),
              'list_controllers': ('GET',
                                   '/api/vcontrollers.xml',
                                   {}),
              'list_servers': ('GET',
                               '/api/servers.xml',
                               {}),
              'list_vol_attachments': ('GET',
                                       '/api/volumes/%s/servers.xml'
                                       % kwargs.get('vpsa_vol'),
                                       {}),
              'list_vol_snapshots': ('GET',
                                     '/api/consistency_groups/%s/snapshots.xml'
                                     % kwargs.get('cg_name'),
                                     {})}
  
          try:
              method, url, params = vpsa_commands[cmd]
          except KeyError:
              raise exception.UnknownCmd(cmd=cmd)
  
          if method == 'GET':
              params = dict(page=1, start=0, limit=0)
              body = None
  
          elif method in ['DELETE', 'POST']:
              body = params
              params = None
  
          else:
              msg = (_('Method %(method)s is not defined') %
                     {'method': method})
              LOG.error(msg)
              raise AssertionError(msg)
  
          # 'access_key' was generated using username and password
          # or it was taken from the input file
          headers = {'X-Access-Key': self.access_key}
  
          return method, url, params, body, headers
  
      def ensure_connection(self, cmd=None):
          """Retrieve access key for VPSA connection."""
  
          if self.access_key or cmd == 'login':
              return
  
          cmd = 'login'
          xml_tree = self.send_cmd(cmd)
          user = xml_tree.find('user')
          if user is None:
              raise (exception.MalformedResponse(
                     cmd=cmd, reason=_('no "user" field')))
          access_key = user.findtext('access-key')
          if access_key is None:
              raise (exception.MalformedResponse(
                     cmd=cmd, reason=_('no "access-key" field')))
          self.access_key = access_key
  
      def send_cmd(self, cmd, **kwargs):
          """Send command to VPSA Controller."""
  
          self.ensure_connection(cmd)
  
          method, url, params, body, headers = self._generate_vpsa_cmd(cmd,
                                                                       **kwargs)
          LOG.debug('Invoking %(cmd)s using %(method)s request.',
                    {'cmd': cmd, 'method': method})
  
          host = self.conf.zadara_vpsa_host
          port = int(self.conf.zadara_vpsa_port)
  
          protocol = "https" if self.conf.zadara_vpsa_use_ssl else "http"
          if protocol == "https":
              if not self.conf.zadara_ssl_cert_verify:
                  verify = False
              else:
                  cert = ((self.conf.driver_ssl_cert_path) or None)
                  verify = cert if cert else True
          else:
              verify = False
  
          if port:
              api_url = "%s://%s:%d%s" % (protocol, host, port, url)
          else:
              api_url = "%s://%s%s" % (protocol, host, url)
  
          try:
              with requests.Session() as session:
                  session.headers.update(headers)
                  response = session.request(method, api_url, params=params,
                                             data=body, headers=headers,
                                             verify=verify)
          except requests.exceptions.RequestException as e:
              message = (_('Exception: %s') % six.text_type(e))
              raise exception.VolumeDriverException(message=message)
  
          if response.status_code != 200:
              raise exception.BadHTTPResponseStatus(status=response.status_code)
  
          data = response.content
          xml_tree = etree.fromstring(data)
          status = xml_tree.findtext('status')
          if status == '5':
              # Invalid Credentials
              raise ZadaraInvalidAccessKey()
  
          if status != '0':
              raise exception.FailedCmdWithDump(status=status, data=data)
  
          if method in ['POST', 'DELETE']:
              LOG.debug('Operation completed with status code %(status)s',
                        {'status': status})
          return xml_tree
  
  
  @interface.volumedriver
  class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
      """Zadara VPSA iSCSI/iSER volume driver.
  
      .. code-block:: none
  
        Version history:
          15.07 - Initial driver
          16.05 - Move from httplib to requests
          19.08 - Add API access key authentication option
      """
  
      VERSION = '19.08'
  
      # ThirdPartySystems wiki page
      CI_WIKI_NAME = "ZadaraStorage_VPSA_CI"
  
      def __init__(self, *args, **kwargs):
          super(ZadaraVPSAISCSIDriver, self).__init__(*args, **kwargs)
          self.vpsa = None
          self.configuration.append_config_values(zadara_opts)
  
      @staticmethod
      def get_driver_options():
          return zadara_opts
  
      def do_setup(self, context):
          """Any initialization the volume driver does while starting.
  
          Establishes initial connection with VPSA and retrieves access_key.
          """
          self.vpsa = ZadaraVPSAConnection(self.configuration)
          self._check_access_key_validity()
  
      def _check_access_key_validity(self):
          """Check VPSA access key"""
          self.vpsa.ensure_connection()
          if not self.vpsa.access_key:
              raise ZadaraInvalidAccessKey()
          active_ctrl = self._get_active_controller_details()
          if active_ctrl is None:
              raise ZadaraInvalidAccessKey()
  
      def check_for_setup_error(self):
          """Returns an error (exception) if prerequisites aren't met."""
          self._check_access_key_validity()
  
      def local_path(self, volume):
          """Return local path to existing local volume."""
          raise NotImplementedError()
  
      def _xml_parse_helper(self, xml_tree, first_level, search_tuple,
                            first=True):
          """Helper for parsing VPSA's XML output.
  
          Returns single item if first==True or list for multiple selection.
          If second argument in search_tuple is None - returns all items with
          appropriate key.
          """
  
          objects = xml_tree.find(first_level)
          if objects is None:
              return None
  
          result_list = []
          key, value = search_tuple
          for child_object in list(objects):
              found_value = child_object.findtext(key)
              if found_value and (found_value == value or value is None):
                  if first:
                      return child_object
                  else:
                      result_list.append(child_object)
          return result_list if result_list else None
  
      def _get_vpsa_volume_name_and_size(self, name):
          """Return VPSA's name & size for the volume."""
          xml_tree = self.vpsa.send_cmd('list_volumes')
          volume = self._xml_parse_helper(xml_tree, 'volumes',
                                          ('display-name', name))
          if volume is not None:
              return (volume.findtext('name'),
                      int(volume.findtext('virtual-capacity')))
  
          return None, None
  
      def _get_vpsa_volume_name(self, name):
          """Return VPSA's name for the volume."""
          (vol_name, size) = self._get_vpsa_volume_name_and_size(name)
          return vol_name
  
      def _get_volume_cg_name(self, name):
          """Return name of the consistency group for the volume.
  
          cg-name is a volume uniqe identifier (legacy attribute)
          and not consistency group as it may imply.
          """
          xml_tree = self.vpsa.send_cmd('list_volumes')
          volume = self._xml_parse_helper(xml_tree, 'volumes',
                                          ('display-name', name))
          if volume is not None:
              return volume.findtext('cg-name')
  
          return None
  
      def _get_snap_id(self, cg_name, snap_name):
          """Return snapshot ID for particular volume."""
          xml_tree = self.vpsa.send_cmd('list_vol_snapshots',
                                        cg_name=cg_name)
          snap = self._xml_parse_helper(xml_tree, 'snapshots',
                                        ('display-name', snap_name))
          if snap is not None:
              return snap.findtext('name')
  
          return None
  
      def _get_pool_capacity(self, pool_name):
          """Return pool's total and available capacities."""
          xml_tree = self.vpsa.send_cmd('list_pools')
          pool = self._xml_parse_helper(xml_tree, 'pools',
                                        ('name', pool_name))
          if pool is not None:
              total = int(pool.findtext('capacity'))
              free = int(float(pool.findtext('available-capacity')))
              LOG.debug('Pool %(name)s: %(total)sGB total, %(free)sGB free',
                        {'name': pool_name, 'total': total, 'free': free})
              return total, free
  
          return 'unknown', 'unknown'
  
      def _get_active_controller_details(self):
          """Return details of VPSA's active controller."""
          xml_tree = self.vpsa.send_cmd('list_controllers')
          ctrl = self._xml_parse_helper(xml_tree, 'vcontrollers',
                                        ('state', 'active'))
          if ctrl is not None:
              return dict(target=ctrl.findtext('target'),
                          ip=ctrl.findtext('iscsi-ip'),
                          chap_user=ctrl.findtext('vpsa-chap-user'),
                          chap_passwd=ctrl.findtext('vpsa-chap-secret'))
          return None
  
      def _detach_vpsa_volume(self, vpsa_vol, vpsa_srv=None):
          """Detach volume from all attached servers."""
          if vpsa_srv:
              list_servers_ids = [vpsa_srv]
          else:
              list_servers = self._get_servers_attached_to_volume(vpsa_vol)
              list_servers_ids = [s.findtext('name') for s in list_servers]
  
          for server_id in list_servers_ids:
              # Detach volume from server
              self.vpsa.send_cmd('detach_volume',
                                 vpsa_srv=server_id,
                                 vpsa_vol=vpsa_vol)
  
      def _get_server_name(self, initiator):
          """Return VPSA's name for server object with given IQN."""
          xml_tree = self.vpsa.send_cmd('list_servers')
          server = self._xml_parse_helper(xml_tree, 'servers',
                                          ('iqn', initiator))
          if server is not None:
              return server.findtext('name')
          return None
  
      def _create_vpsa_server(self, initiator):
          """Create server object within VPSA (if doesn't exist)."""
          vpsa_srv = self._get_server_name(initiator)
          if not vpsa_srv:
              xml_tree = self.vpsa.send_cmd('create_server', initiator=initiator)
              vpsa_srv = xml_tree.findtext('server-name')
          return vpsa_srv
  
      def create_volume(self, volume):
          """Create volume."""
          self.vpsa.send_cmd(
              'create_volume',
              name=self.configuration.zadara_vol_name_template % volume['name'],
              size=volume['size'])
  
      def delete_volume(self, volume):
          """Delete volume.
  
          Return ok if doesn't exist. Auto detach from all servers.
          """
          # Get volume name
          name = self.configuration.zadara_vol_name_template % volume['name']
          vpsa_vol = self._get_vpsa_volume_name(name)
          if not vpsa_vol:
              LOG.warning('Volume %s could not be found. '
                          'It might be already deleted', name)
              return
  
          self._detach_vpsa_volume(vpsa_vol=vpsa_vol)
  
          # Delete volume
          self.vpsa.send_cmd('delete_volume', vpsa_vol=vpsa_vol)
  
      def create_snapshot(self, snapshot):
          """Creates a snapshot."""
  
          LOG.debug('Create snapshot: %s', snapshot['name'])
  
          # Retrieve the CG name for the base volume
          volume_name = (self.configuration.zadara_vol_name_template
                         % snapshot['volume_name'])
          cg_name = self._get_volume_cg_name(volume_name)
          if not cg_name:
              msg = _('Volume %(name)s not found') % {'name': volume_name}
              LOG.error(msg)
              raise exception.VolumeDriverException(message=msg)
  
          self.vpsa.send_cmd('create_snapshot',
                             cg_name=cg_name,
                             snap_name=snapshot['name'])
  
      def delete_snapshot(self, snapshot):
          """Deletes a snapshot."""
  
          LOG.debug('Delete snapshot: %s', snapshot['name'])
  
          # Retrieve the CG name for the base volume
          volume_name = (self.configuration.zadara_vol_name_template
                         % snapshot['volume_name'])
          cg_name = self._get_volume_cg_name(volume_name)
          if not cg_name:
              # If the volume isn't present, then don't attempt to delete
              LOG.warning('snapshot: original volume %s not found, '
                          'skipping delete operation',
                          volume_name)
              return
  
          snap_id = self._get_snap_id(cg_name, snapshot['name'])
          if not snap_id:
              # If the snapshot isn't present, then don't attempt to delete
              LOG.warning('snapshot: snapshot %s not found, '
                          'skipping delete operation', snapshot['name'])
              return
  
          self.vpsa.send_cmd('delete_snapshot',
                             snap_id=snap_id)
  
      def create_volume_from_snapshot(self, volume, snapshot):
          """Creates a volume from a snapshot."""
  
          LOG.debug('Creating volume from snapshot: %s', snapshot['name'])
  
          # Retrieve the CG name for the base volume
          volume_name = (self.configuration.zadara_vol_name_template
                         % snapshot['volume_name'])
          cg_name = self._get_volume_cg_name(volume_name)
          if not cg_name:
              LOG.error('Volume %(name)s not found', {'name': volume_name})
              raise exception.VolumeNotFound(volume_id=volume['id'])
  
          snap_id = self._get_snap_id(cg_name, snapshot['name'])
          if not snap_id:
              LOG.error('Snapshot %(name)s not found',
                        {'name': snapshot['name']})
              raise exception.SnapshotNotFound(snapshot_id=snapshot['id'])
  
          self.vpsa.send_cmd('create_clone_from_snap',
                             cg_name=cg_name,
                             name=self.configuration.zadara_vol_name_template
                             % volume['name'],
                             snap_id=snap_id)
  
          if volume['size'] > snapshot['volume_size']:
              self.extend_volume(volume, volume['size'])
  
      def create_cloned_volume(self, volume, src_vref):
          """Creates a clone of the specified volume."""
  
          LOG.debug('Creating clone of volume: %s', src_vref['name'])
  
          # Retrieve the CG name for the base volume
          volume_name = (self.configuration.zadara_vol_name_template
                         % src_vref['name'])
          cg_name = self._get_volume_cg_name(volume_name)
          if not cg_name:
              LOG.error('Volume %(name)s not found', {'name': volume_name})
              raise exception.VolumeNotFound(volume_id=volume['id'])
  
          self.vpsa.send_cmd('create_clone',
                             cg_name=cg_name,
                             name=self.configuration.zadara_vol_name_template
                             % volume['name'])
  
          if volume['size'] > src_vref['size']:
              self.extend_volume(volume, volume['size'])
  
      def extend_volume(self, volume, new_size):
          """Extend an existing volume."""
          # Get volume name
          name = self.configuration.zadara_vol_name_template % volume['name']
          (vpsa_vol, size) = self._get_vpsa_volume_name_and_size(name)
          if not vpsa_vol:
              msg = (_('Volume %(name)s could not be found. '
                       'It might be already deleted') % {'name': name})
              LOG.error(msg)
              raise ZadaraVolumeNotFound(reason=msg)
  
          if new_size < size:
              raise exception.InvalidInput(
                  reason=_('%(new_size)s < current size %(size)s') %
                  {'new_size': new_size, 'size': size})
  
          expand_size = new_size - size
          self.vpsa.send_cmd('expand_volume',
                             vpsa_vol=vpsa_vol,
                             size=expand_size)
  
      def create_export(self, context, volume, vg=None):
          """Irrelevant for VPSA volumes. Export created during attachment."""
          pass
  
      def ensure_export(self, context, volume):
          """Irrelevant for VPSA volumes. Export created during attachment."""
          pass
  
      def remove_export(self, context, volume):
          """Irrelevant for VPSA volumes. Export removed during detach."""
          pass
  
      def initialize_connection(self, volume, connector):
          """Attach volume to initiator/host.
  
          During this call VPSA exposes volume to particular Initiator. It also
          creates a 'server' entity for Initiator (if it was not created before)
          All necessary connection information is returned, including auth data.
          Connection data (target, LUN) is not stored in the DB.
          """
          # First: Check Active controller: if not valid, raise exception
          ctrl = self._get_active_controller_details()
          if not ctrl:
              raise ZadaraVPSANoActiveController()
  
          # Get/Create server name for IQN
          initiator_name = connector['initiator']
          vpsa_srv = self._create_vpsa_server(initiator_name)
          if not vpsa_srv:
              raise ZadaraServerCreateFailure(name=initiator_name)
  
          # Get volume name
          name = self.configuration.zadara_vol_name_template % volume['name']
          vpsa_vol = self._get_vpsa_volume_name(name)
          if not vpsa_vol:
              raise exception.VolumeNotFound(volume_id=volume['id'])
  
          xml_tree = self.vpsa.send_cmd('list_vol_attachments',
                                        vpsa_vol=vpsa_vol)
          attach = self._xml_parse_helper(xml_tree, 'servers',
                                          ('name', vpsa_srv))
          # Attach volume to server
          if attach is None:
              self.vpsa.send_cmd('attach_volume',
                                 vpsa_srv=vpsa_srv,
                                 vpsa_vol=vpsa_vol)
  
          xml_tree = self.vpsa.send_cmd('list_vol_attachments',
                                        vpsa_vol=vpsa_vol)
          server = self._xml_parse_helper(xml_tree, 'servers',
                                          ('iqn', initiator_name))
          if server is None:
              raise ZadaraAttachmentsNotFound(name=name)
  
          target = server.findtext('target')
          lun = int(server.findtext('lun'))
          if None in [target, lun]:
              raise ZadaraInvalidAttachmentInfo(
                  name=name,
                  reason=_('target=%(target)s, lun=%(lun)s') %
                  {'target': target, 'lun': lun})
  
          properties = {'target_discovered': False,
                        'target_portal': '%s:%s' % (ctrl['ip'], '3260'),
                        'target_iqn': target,
                        'target_lun': lun,
                        'volume_id': volume['id'],
                        'auth_method': 'CHAP',
                        'auth_username': ctrl['chap_user'],
                        'auth_password': ctrl['chap_passwd']}
  
          LOG.debug('Attach properties: %(properties)s',
                    {'properties': strutils.mask_password(properties)})
          return {'driver_volume_type':
                  ('iser' if (self.configuration.safe_get('zadara_use_iser'))
                   else 'iscsi'), 'data': properties}
  
      def terminate_connection(self, volume, connector, **kwargs):
          """Detach volume from the initiator."""
  
          # Get server name for IQN
          if connector is None:
              # Detach volume from all servers
              # Get volume name
              name = self.configuration.zadara_vol_name_template % volume['name']
              vpsa_vol = self._get_vpsa_volume_name(name)
              if vpsa_vol:
                  self._detach_vpsa_volume(vpsa_vol=vpsa_vol)
                  return
              else:
                  LOG.warning('Volume %s could not be found', name)
                  raise exception.VolumeNotFound(volume_id=volume['id'])
  
          initiator_name = connector['initiator']
  
          vpsa_srv = self._get_server_name(initiator_name)
          if not vpsa_srv:
              raise ZadaraServerNotFound(name=initiator_name)
  
          # Get volume name
          name = self.configuration.zadara_vol_name_template % volume['name']
          vpsa_vol = self._get_vpsa_volume_name(name)
          if not vpsa_vol:
              raise exception.VolumeNotFound(volume_id=volume['id'])
  
          # Detach volume from server
          self._detach_vpsa_volume(vpsa_vol=vpsa_vol, vpsa_srv=vpsa_srv)
  
      def _get_servers_attached_to_volume(self, vpsa_vol):
          """Return all servers attached to volume."""
          xml_tree = self.vpsa.send_cmd('list_vol_attachments',
                                        vpsa_vol=vpsa_vol)
          list_servers = self._xml_parse_helper(xml_tree, 'servers',
                                                ('iqn', None), first=False)
          return list_servers or []
  
      def _update_volume_stats(self):
          """Retrieve stats info from volume group."""
          LOG.debug("Updating volume stats")
          data = {}
          backend_name = self.configuration.safe_get('volume_backend_name')
          storage_protocol = ('iSER' if
                              (self.configuration.safe_get('zadara_use_iser'))
                              else 'iSCSI')
          data["volume_backend_name"] = backend_name or self.__class__.__name__
          data["vendor_name"] = 'Zadara Storage'
          data["driver_version"] = self.VERSION
          data["storage_protocol"] = storage_protocol
          data['reserved_percentage'] = self.configuration.reserved_percentage
          data['QoS_support'] = False
  
          (total, free) = self._get_pool_capacity(self.configuration.
                                                  zadara_vpsa_poolname)
          data['total_capacity_gb'] = total
          data['free_capacity_gb'] = free
  
          self._stats = data
  