1# Copyright (c) 2016 Zadara Storage, Inc.
2# All Rights Reserved.
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15"""
16Volume driver for Zadara Virtual Private Storage Array (VPSA).
17
18This driver requires VPSA with API version 15.07 or higher.
19"""
20
21from defusedxml import lxml as etree
22from oslo_config import cfg
23from oslo_log import log as logging
24from oslo_utils import strutils
25import requests
26import six
27
28from cinder import exception
29from cinder.i18n import _
30from cinder import interface
31from cinder.volume import configuration
32from cinder.volume import driver
33
34LOG = logging.getLogger(__name__)
35
36zadara_opts = [
37    cfg.BoolOpt('zadara_use_iser',
38                default=True,
39                help='VPSA - Use ISER instead of iSCSI'),
40    cfg.StrOpt('zadara_vpsa_host',
41               default=None,
42               help='VPSA - Management Host name or IP address'),
43    cfg.PortOpt('zadara_vpsa_port',
44                default=None,
45                help='VPSA - Port number'),
46    cfg.BoolOpt('zadara_vpsa_use_ssl',
47                default=False,
48                help='VPSA - Use SSL connection'),
49    cfg.BoolOpt('zadara_ssl_cert_verify',
50                default=True,
51                help='If set to True the http client will validate the SSL '
52                     'certificate of the VPSA endpoint.'),
53    cfg.StrOpt('zadara_user',
54               default=None,
55               help='VPSA - Username'),
56    cfg.StrOpt('zadara_password',
57               default=None,
58               help='VPSA - Password',
59               secret=True),
60    cfg.StrOpt('zadara_vpsa_poolname',
61               default=None,
62               help='VPSA - Storage Pool assigned for volumes'),
63    cfg.BoolOpt('zadara_vol_encrypt',
64                default=False,
65                help='VPSA - Default encryption policy for volumes'),
66    cfg.StrOpt('zadara_vol_name_template',
67               default='OS_%s',
68               help='VPSA - Default template for VPSA volume names'),
69    cfg.BoolOpt('zadara_default_snap_policy',
70                default=False,
71                help="VPSA - Attach snapshot policy for volumes")]
72
73CONF = cfg.CONF
74CONF.register_opts(zadara_opts, group=configuration.SHARED_CONF_GROUP)
75
76
77class ZadaraVPSAConnection(object):
78    """Executes volume driver commands on VPSA."""
79
80    def __init__(self, conf):
81        self.conf = conf
82        self.access_key = None
83
84        self.ensure_connection()
85
86    def _generate_vpsa_cmd(self, cmd, **kwargs):
87        """Generate command to be sent to VPSA."""
88
89        def _joined_params(params):
90            param_str = []
91            for k, v in params.items():
92                param_str.append("%s=%s" % (k, v))
93            return '&'.join(param_str)
94
95        # Dictionary of applicable VPSA commands in the following format:
96        # 'command': (method, API_URL, {optional parameters})
97        vpsa_commands = {
98            'login': ('POST',
99                      '/api/users/login.xml',
100                      {'user': self.conf.zadara_user,
101                       'password': self.conf.zadara_password}),
102
103            # Volume operations
104            'create_volume': ('POST',
105                              '/api/volumes.xml',
106                              {'name': kwargs.get('name'),
107                               'capacity': kwargs.get('size'),
108                               'pool': self.conf.zadara_vpsa_poolname,
109                               'thin': 'YES',
110                               'crypt': 'YES'
111                               if self.conf.zadara_vol_encrypt else 'NO',
112                               'attachpolicies': 'NO'
113                               if not self.conf.zadara_default_snap_policy
114                               else 'YES'}),
115            'delete_volume': ('DELETE',
116                              '/api/volumes/%s.xml' % kwargs.get('vpsa_vol'),
117                              {'force': 'YES'}),
118            'expand_volume': ('POST',
119                              '/api/volumes/%s/expand.xml'
120                              % kwargs.get('vpsa_vol'),
121                              {'capacity': kwargs.get('size')}),
122
123            # Snapshot operations
124            # Snapshot request is triggered for a single volume though the
125            # API call implies that snapshot is triggered for CG (legacy API).
126            'create_snapshot': ('POST',
127                                '/api/consistency_groups/%s/snapshots.xml'
128                                % kwargs.get('cg_name'),
129                                {'display_name': kwargs.get('snap_name')}),
130            'delete_snapshot': ('DELETE',
131                                '/api/snapshots/%s.xml'
132                                % kwargs.get('snap_id'),
133                                {}),
134
135            'create_clone_from_snap': ('POST',
136                                       '/api/consistency_groups/%s/clone.xml'
137                                       % kwargs.get('cg_name'),
138                                       {'name': kwargs.get('name'),
139                                        'snapshot': kwargs.get('snap_id')}),
140
141            'create_clone': ('POST',
142                             '/api/consistency_groups/%s/clone.xml'
143                             % kwargs.get('cg_name'),
144                             {'name': kwargs.get('name')}),
145
146            # Server operations
147            'create_server': ('POST',
148                              '/api/servers.xml',
149                              {'display_name': kwargs.get('initiator'),
150                               'iqn': kwargs.get('initiator')}),
151
152            # Attach/Detach operations
153            'attach_volume': ('POST',
154                              '/api/servers/%s/volumes.xml'
155                              % kwargs.get('vpsa_srv'),
156                              {'volume_name[]': kwargs.get('vpsa_vol'),
157                               'force': 'NO'}),
158            'detach_volume': ('POST',
159                              '/api/volumes/%s/detach.xml'
160                              % kwargs.get('vpsa_vol'),
161                              {'server_name[]': kwargs.get('vpsa_srv'),
162                               'force': 'NO'}),
163
164            # Get operations
165            'list_volumes': ('GET',
166                             '/api/volumes.xml',
167                             {}),
168            'list_pools': ('GET',
169                           '/api/pools.xml',
170                           {}),
171            'list_controllers': ('GET',
172                                 '/api/vcontrollers.xml',
173                                 {}),
174            'list_servers': ('GET',
175                             '/api/servers.xml',
176                             {}),
177            'list_vol_attachments': ('GET',
178                                     '/api/volumes/%s/servers.xml'
179                                     % kwargs.get('vpsa_vol'),
180                                     {}),
181            'list_vol_snapshots': ('GET',
182                                   '/api/consistency_groups/%s/snapshots.xml'
183                                   % kwargs.get('cg_name'),
184                                   {})}
185
186        if cmd not in vpsa_commands:
187            raise exception.UnknownCmd(cmd=cmd)
188        else:
189            (method, url, params) = vpsa_commands[cmd]
190
191        if method == 'GET':
192            # For GET commands add parameters to the URL
193            params.update(dict(access_key=self.access_key,
194                               page=1, start=0, limit=0))
195            url += '?' + _joined_params(params)
196            body = ''
197
198        elif method == 'DELETE':
199            # For DELETE commands add parameters to the URL
200            params.update(dict(access_key=self.access_key))
201            url += '?' + _joined_params(params)
202            body = ''
203
204        elif method == 'POST':
205            if self.access_key:
206                params.update(dict(access_key=self.access_key))
207            body = _joined_params(params)
208
209        else:
210            msg = (_('Method %(method)s is not defined') %
211                   {'method': method})
212            LOG.error(msg)
213            raise AssertionError(msg)
214
215        return (method, url, body)
216
217    def ensure_connection(self, cmd=None):
218        """Retrieve access key for VPSA connection."""
219
220        if self.access_key or cmd == 'login':
221            return
222
223        cmd = 'login'
224        xml_tree = self.send_cmd(cmd)
225        user = xml_tree.find('user')
226        if user is None:
227            raise (exception.MalformedResponse(cmd=cmd,
228                   reason=_('no "user" field')))
229        access_key = user.findtext('access-key')
230        if access_key is None:
231            raise (exception.MalformedResponse(cmd=cmd,
232                   reason=_('no "access-key" field')))
233        self.access_key = access_key
234
235    def send_cmd(self, cmd, **kwargs):
236        """Send command to VPSA Controller."""
237
238        self.ensure_connection(cmd)
239
240        (method, url, body) = self._generate_vpsa_cmd(cmd, **kwargs)
241        LOG.debug('Invoking %(cmd)s using %(method)s request.',
242                  {'cmd': cmd, 'method': method})
243
244        host = self.conf.zadara_vpsa_host
245        port = int(self.conf.zadara_vpsa_port)
246
247        protocol = "https" if self.conf.zadara_vpsa_use_ssl else "http"
248        if protocol == "https":
249            if not self.conf.zadara_ssl_cert_verify:
250                verify = False
251            else:
252                cert = ((self.conf.driver_ssl_cert_path) or None)
253                verify = cert if cert else True
254        else:
255            verify = False
256
257        if port:
258            api_url = "%s://%s:%d%s" % (protocol, host, port, url)
259        else:
260            api_url = "%s://%s%s" % (protocol, host, url)
261
262        try:
263            response = requests.request(method, api_url, data=body,
264                                        verify=verify)
265        except requests.exceptions.RequestException as e:
266            message = (_('Exception: %s') % six.text_type(e))
267            raise exception.VolumeDriverException(message=message)
268
269        if response.status_code != 200:
270            raise exception.BadHTTPResponseStatus(status=response.status_code)
271
272        data = response.content
273        xml_tree = etree.fromstring(data)
274        status = xml_tree.findtext('status')
275        if status != '0':
276            raise exception.FailedCmdWithDump(status=status, data=data)
277
278        if method in ['POST', 'DELETE']:
279            LOG.debug('Operation completed with status code %(status)s',
280                      {'status': status})
281        return xml_tree
282
283
284@interface.volumedriver
285class ZadaraVPSAISCSIDriver(driver.ISCSIDriver):
286    """Zadara VPSA iSCSI/iSER volume driver.
287
288    Version history:
289        15.07 - Initial driver
290        16.05 - Move from httplib to requests
291    """
292
293    VERSION = '16.05'
294
295    # ThirdPartySystems wiki page
296    CI_WIKI_NAME = "ZadaraStorage_VPSA_CI"
297
298    def __init__(self, *args, **kwargs):
299        super(ZadaraVPSAISCSIDriver, self).__init__(*args, **kwargs)
300        self.configuration.append_config_values(zadara_opts)
301
302    def do_setup(self, context):
303        """Any initialization the volume driver does while starting.
304
305        Establishes initial connection with VPSA and retrieves access_key.
306        """
307        self.vpsa = ZadaraVPSAConnection(self.configuration)
308
309    def check_for_setup_error(self):
310        """Returns an error (exception) if prerequisites aren't met."""
311        self.vpsa.ensure_connection()
312
313    def local_path(self, volume):
314        """Return local path to existing local volume."""
315        raise NotImplementedError()
316
317    def _xml_parse_helper(self, xml_tree, first_level, search_tuple,
318                          first=True):
319        """Helper for parsing VPSA's XML output.
320
321        Returns single item if first==True or list for multiple selection.
322        If second argument in search_tuple is None - returns all items with
323        appropriate key.
324        """
325
326        objects = xml_tree.find(first_level)
327        if objects is None:
328            return None
329
330        result_list = []
331        (key, value) = search_tuple
332        for object in objects.getchildren():
333            found_value = object.findtext(key)
334            if found_value and (found_value == value or value is None):
335                if first:
336                    return object
337                else:
338                    result_list.append(object)
339        return result_list if result_list else None
340
341    def _get_vpsa_volume_name_and_size(self, name):
342        """Return VPSA's name & size for the volume."""
343        xml_tree = self.vpsa.send_cmd('list_volumes')
344        volume = self._xml_parse_helper(xml_tree, 'volumes',
345                                        ('display-name', name))
346        if volume is not None:
347            return (volume.findtext('name'),
348                    int(volume.findtext('virtual-capacity')))
349
350        return (None, None)
351
352    def _get_vpsa_volume_name(self, name):
353        """Return VPSA's name for the volume."""
354        (vol_name, size) = self._get_vpsa_volume_name_and_size(name)
355        return vol_name
356
357    def _get_volume_cg_name(self, name):
358        """Return name of the consistency group for the volume.
359
360        cg-name is a volume uniqe identifier (legacy attribute)
361        and not consistency group as it may imply.
362        """
363        xml_tree = self.vpsa.send_cmd('list_volumes')
364        volume = self._xml_parse_helper(xml_tree, 'volumes',
365                                        ('display-name', name))
366        if volume is not None:
367            return volume.findtext('cg-name')
368
369        return None
370
371    def _get_snap_id(self, cg_name, snap_name):
372        """Return snapshot ID for particular volume."""
373        xml_tree = self.vpsa.send_cmd('list_vol_snapshots',
374                                      cg_name=cg_name)
375        snap = self._xml_parse_helper(xml_tree, 'snapshots',
376                                      ('display-name', snap_name))
377        if snap is not None:
378            return snap.findtext('name')
379
380        return None
381
382    def _get_pool_capacity(self, pool_name):
383        """Return pool's total and available capacities."""
384        xml_tree = self.vpsa.send_cmd('list_pools')
385        pool = self._xml_parse_helper(xml_tree, 'pools',
386                                      ('name', pool_name))
387        if pool is not None:
388            total = int(pool.findtext('capacity'))
389            free = int(float(pool.findtext('available-capacity')))
390            LOG.debug('Pool %(name)s: %(total)sGB total, %(free)sGB free',
391                      {'name': pool_name, 'total': total, 'free': free})
392            return (total, free)
393
394        return ('unknown', 'unknown')
395
396    def _get_active_controller_details(self):
397        """Return details of VPSA's active controller."""
398        xml_tree = self.vpsa.send_cmd('list_controllers')
399        ctrl = self._xml_parse_helper(xml_tree, 'vcontrollers',
400                                      ('state', 'active'))
401        if ctrl is not None:
402            return dict(target=ctrl.findtext('target'),
403                        ip=ctrl.findtext('iscsi-ip'),
404                        chap_user=ctrl.findtext('vpsa-chap-user'),
405                        chap_passwd=ctrl.findtext('vpsa-chap-secret'))
406        return None
407
408    def _get_server_name(self, initiator):
409        """Return VPSA's name for server object with given IQN."""
410        xml_tree = self.vpsa.send_cmd('list_servers')
411        server = self._xml_parse_helper(xml_tree, 'servers',
412                                        ('iqn', initiator))
413        if server is not None:
414            return server.findtext('name')
415        return None
416
417    def _create_vpsa_server(self, initiator):
418        """Create server object within VPSA (if doesn't exist)."""
419        vpsa_srv = self._get_server_name(initiator)
420        if not vpsa_srv:
421            xml_tree = self.vpsa.send_cmd('create_server', initiator=initiator)
422            vpsa_srv = xml_tree.findtext('server-name')
423        return vpsa_srv
424
425    def create_volume(self, volume):
426        """Create volume."""
427        self.vpsa.send_cmd(
428            'create_volume',
429            name=self.configuration.zadara_vol_name_template % volume['name'],
430            size=volume['size'])
431
432    def delete_volume(self, volume):
433        """Delete volume.
434
435        Return ok if doesn't exist. Auto detach from all servers.
436        """
437        # Get volume name
438        name = self.configuration.zadara_vol_name_template % volume['name']
439        vpsa_vol = self._get_vpsa_volume_name(name)
440        if not vpsa_vol:
441            LOG.warning('Volume %s could not be found. '
442                        'It might be already deleted', name)
443            return
444
445        # Check attachment info and detach from all
446        xml_tree = self.vpsa.send_cmd('list_vol_attachments',
447                                      vpsa_vol=vpsa_vol)
448        servers = self._xml_parse_helper(xml_tree, 'servers',
449                                         ('iqn', None), first=False)
450        if servers:
451            for server in servers:
452                vpsa_srv = server.findtext('name')
453                if vpsa_srv:
454                    self.vpsa.send_cmd('detach_volume',
455                                       vpsa_srv=vpsa_srv,
456                                       vpsa_vol=vpsa_vol)
457
458        # Delete volume
459        self.vpsa.send_cmd('delete_volume', vpsa_vol=vpsa_vol)
460
461    def create_snapshot(self, snapshot):
462        """Creates a snapshot."""
463
464        LOG.debug('Create snapshot: %s', snapshot['name'])
465
466        # Retrieve the CG name for the base volume
467        volume_name = (self.configuration.zadara_vol_name_template
468                       % snapshot['volume_name'])
469        cg_name = self._get_volume_cg_name(volume_name)
470        if not cg_name:
471            msg = _('Volume %(name)s not found') % {'name': volume_name}
472            LOG.error(msg)
473            raise exception.VolumeDriverException(message=msg)
474
475        self.vpsa.send_cmd('create_snapshot',
476                           cg_name=cg_name,
477                           snap_name=snapshot['name'])
478
479    def delete_snapshot(self, snapshot):
480        """Deletes a snapshot."""
481
482        LOG.debug('Delete snapshot: %s', snapshot['name'])
483
484        # Retrieve the CG name for the base volume
485        volume_name = (self.configuration.zadara_vol_name_template
486                       % snapshot['volume_name'])
487        cg_name = self._get_volume_cg_name(volume_name)
488        if not cg_name:
489            # If the volume isn't present, then don't attempt to delete
490            LOG.warning('snapshot: original volume %s not found, '
491                        'skipping delete operation',
492                        volume_name)
493            return
494
495        snap_id = self._get_snap_id(cg_name, snapshot['name'])
496        if not snap_id:
497            # If the snapshot isn't present, then don't attempt to delete
498            LOG.warning('snapshot: snapshot %s not found, '
499                        'skipping delete operation', snapshot['name'])
500            return
501
502        self.vpsa.send_cmd('delete_snapshot',
503                           snap_id=snap_id)
504
505    def create_volume_from_snapshot(self, volume, snapshot):
506        """Creates a volume from a snapshot."""
507
508        LOG.debug('Creating volume from snapshot: %s', snapshot['name'])
509
510        # Retrieve the CG name for the base volume
511        volume_name = (self.configuration.zadara_vol_name_template
512                       % snapshot['volume_name'])
513        cg_name = self._get_volume_cg_name(volume_name)
514        if not cg_name:
515            LOG.error('Volume %(name)s not found', {'name': volume_name})
516            raise exception.VolumeNotFound(volume_id=volume['id'])
517
518        snap_id = self._get_snap_id(cg_name, snapshot['name'])
519        if not snap_id:
520            LOG.error('Snapshot %(name)s not found',
521                      {'name': snapshot['name']})
522            raise exception.SnapshotNotFound(snapshot_id=snapshot['id'])
523
524        self.vpsa.send_cmd('create_clone_from_snap',
525                           cg_name=cg_name,
526                           name=self.configuration.zadara_vol_name_template
527                           % volume['name'],
528                           snap_id=snap_id)
529
530        if (volume['size'] > snapshot['volume_size']):
531            self.extend_volume(volume, volume['size'])
532
533    def create_cloned_volume(self, volume, src_vref):
534        """Creates a clone of the specified volume."""
535
536        LOG.debug('Creating clone of volume: %s', src_vref['name'])
537
538        # Retrieve the CG name for the base volume
539        volume_name = (self.configuration.zadara_vol_name_template
540                       % src_vref['name'])
541        cg_name = self._get_volume_cg_name(volume_name)
542        if not cg_name:
543            LOG.error('Volume %(name)s not found', {'name': volume_name})
544            raise exception.VolumeNotFound(volume_id=volume['id'])
545
546        self.vpsa.send_cmd('create_clone',
547                           cg_name=cg_name,
548                           name=self.configuration.zadara_vol_name_template
549                           % volume['name'])
550
551        if (volume['size'] > src_vref['size']):
552            self.extend_volume(volume, volume['size'])
553
554    def extend_volume(self, volume, new_size):
555        """Extend an existing volume."""
556        # Get volume name
557        name = self.configuration.zadara_vol_name_template % volume['name']
558        (vpsa_vol, size) = self._get_vpsa_volume_name_and_size(name)
559        if not vpsa_vol:
560            msg = (_('Volume %(name)s could not be found. '
561                     'It might be already deleted') % {'name': name})
562            LOG.error(msg)
563            raise exception.ZadaraVolumeNotFound(reason=msg)
564
565        if new_size < size:
566            raise exception.InvalidInput(
567                reason=_('%(new_size)s < current size %(size)s') %
568                {'new_size': new_size, 'size': size})
569
570        expand_size = new_size - size
571        self.vpsa.send_cmd('expand_volume',
572                           vpsa_vol=vpsa_vol,
573                           size=expand_size)
574
575    def create_export(self, context, volume, vg=None):
576        """Irrelevant for VPSA volumes. Export created during attachment."""
577        pass
578
579    def ensure_export(self, context, volume):
580        """Irrelevant for VPSA volumes. Export created during attachment."""
581        pass
582
583    def remove_export(self, context, volume):
584        """Irrelevant for VPSA volumes. Export removed during detach."""
585        pass
586
587    def initialize_connection(self, volume, connector):
588        """Attach volume to initiator/host.
589
590        During this call VPSA exposes volume to particular Initiator. It also
591        creates a 'server' entity for Initiator (if it was not created before)
592
593        All necessary connection information is returned, including auth data.
594        Connection data (target, LUN) is not stored in the DB.
595        """
596
597        # Get/Create server name for IQN
598        initiator_name = connector['initiator']
599        vpsa_srv = self._create_vpsa_server(initiator_name)
600        if not vpsa_srv:
601            raise exception.ZadaraServerCreateFailure(name=initiator_name)
602
603        # Get volume name
604        name = self.configuration.zadara_vol_name_template % volume['name']
605        vpsa_vol = self._get_vpsa_volume_name(name)
606        if not vpsa_vol:
607            raise exception.VolumeNotFound(volume_id=volume['id'])
608
609        # Get Active controller details
610        ctrl = self._get_active_controller_details()
611        if not ctrl:
612            raise exception.ZadaraVPSANoActiveController()
613
614        xml_tree = self.vpsa.send_cmd('list_vol_attachments',
615                                      vpsa_vol=vpsa_vol)
616        attach = self._xml_parse_helper(xml_tree, 'servers',
617                                        ('name', vpsa_srv))
618        # Attach volume to server
619        if attach is None:
620            self.vpsa.send_cmd('attach_volume',
621                               vpsa_srv=vpsa_srv,
622                               vpsa_vol=vpsa_vol)
623        # Get connection info
624        xml_tree = self.vpsa.send_cmd('list_vol_attachments',
625                                      vpsa_vol=vpsa_vol)
626        server = self._xml_parse_helper(xml_tree, 'servers',
627                                        ('iqn', initiator_name))
628        if server is None:
629            raise exception.ZadaraAttachmentsNotFound(name=name)
630        target = server.findtext('target')
631        lun = int(server.findtext('lun'))
632        if target is None or lun is None:
633            raise exception.ZadaraInvalidAttachmentInfo(
634                name=name,
635                reason=_('target=%(target)s, lun=%(lun)s') %
636                {'target': target, 'lun': lun})
637
638        properties = {}
639        properties['target_discovered'] = False
640        properties['target_portal'] = '%s:%s' % (ctrl['ip'], '3260')
641        properties['target_iqn'] = target
642        properties['target_lun'] = lun
643        properties['volume_id'] = volume['id']
644        properties['auth_method'] = 'CHAP'
645        properties['auth_username'] = ctrl['chap_user']
646        properties['auth_password'] = ctrl['chap_passwd']
647
648        LOG.debug('Attach properties: %(properties)s',
649                  {'properties': strutils.mask_password(properties)})
650        return {'driver_volume_type':
651                ('iser' if (self.configuration.safe_get('zadara_use_iser'))
652                 else 'iscsi'), 'data': properties}
653
654    def terminate_connection(self, volume, connector, **kwargs):
655        """Detach volume from the initiator."""
656        # Get server name for IQN
657        initiator_name = connector['initiator']
658        vpsa_srv = self._get_server_name(initiator_name)
659        if not vpsa_srv:
660            raise exception.ZadaraServerNotFound(name=initiator_name)
661
662        # Get volume name
663        name = self.configuration.zadara_vol_name_template % volume['name']
664        vpsa_vol = self._get_vpsa_volume_name(name)
665        if not vpsa_vol:
666            raise exception.VolumeNotFound(volume_id=volume['id'])
667
668        # Detach volume from server
669        self.vpsa.send_cmd('detach_volume',
670                           vpsa_srv=vpsa_srv,
671                           vpsa_vol=vpsa_vol)
672
673    def get_volume_stats(self, refresh=False):
674        """Get volume stats.
675
676        If 'refresh' is True, run update the stats first.
677        """
678
679        if refresh:
680            self._update_volume_stats()
681
682        return self._stats
683
684    def _update_volume_stats(self):
685        """Retrieve stats info from volume group."""
686
687        LOG.debug("Updating volume stats")
688        data = {}
689        backend_name = self.configuration.safe_get('volume_backend_name')
690        storage_protocol = ('iSER' if
691                            (self.configuration.safe_get('zadara_use_iser'))
692                            else 'iSCSI')
693        data["volume_backend_name"] = backend_name or self.__class__.__name__
694        data["vendor_name"] = 'Zadara Storage'
695        data["driver_version"] = self.VERSION
696        data["storage_protocol"] = storage_protocol
697        data['reserved_percentage'] = self.configuration.reserved_percentage
698        data['QoS_support'] = False
699
700        (total, free) = self._get_pool_capacity(self.configuration.
701                                                zadara_vpsa_poolname)
702        data['total_capacity_gb'] = total
703        data['free_capacity_gb'] = free
704
705        self._stats = data
706