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