# 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 objects.getchildren():
            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
