Coverage for manila/share/drivers/hpe/hpe_3par_driver.py: 95%
243 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright 2015 Hewlett Packard Enterprise Development LP
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
15"""HPE 3PAR Driver for OpenStack Manila."""
17import datetime
18import hashlib
19import inspect
20import os
21import re
23from oslo_config import cfg
24from oslo_config import types
25from oslo_log import log
27from manila.common import config
28from manila import exception
29from manila.i18n import _
30from manila.share import driver
31from manila.share.drivers.hpe import hpe_3par_mediator
32from manila.share import share_types
33from manila.share import utils as share_utils
34from manila import utils
36LOG = log.getLogger(__name__)
39class FPG(types.String, types.IPAddress):
40 """FPG type.
42 Used to represent multiple pools per backend values.
43 Converts configuration value to an FPGs value.
44 FPGs value format::
46 FPG name, IP address 1, IP address 2, ..., IP address 4
48 where FPG name is a string value,
49 IP address is of type types.IPAddress
51 Optionally doing range checking.
52 If value is whitespace or empty string will raise error
54 :param min_ip: Optional check that number of min IP address of VFS.
55 :param max_ip: Optional check that number of max IP address of VFS.
56 :param type_name: Type name to be used in the sample config file.
58 """
60 MAX_SUPPORTED_IP_PER_VFS = 4
62 def __init__(self, min_ip=0, max_ip=MAX_SUPPORTED_IP_PER_VFS,
63 type_name='FPG'):
64 types.String.__init__(self, type_name=type_name)
65 types.IPAddress.__init__(self, type_name=type_name)
67 if max_ip < min_ip:
68 msg = _("Pool's max acceptable IP cannot be less than min.")
69 raise exception.HPE3ParInvalid(err=msg)
71 if min_ip < 0:
72 msg = _("Pools must be configured with zero or more IPs.")
73 raise exception.HPE3ParInvalid(err=msg)
75 if max_ip > FPG.MAX_SUPPORTED_IP_PER_VFS:
76 msg = (_("Pool's max acceptable IP cannot be greater than "
77 "supported value=%s.") % FPG.MAX_SUPPORTED_IP_PER_VFS)
78 raise exception.HPE3ParInvalid(err=msg)
80 self.min_ip = min_ip
81 self.max_ip = max_ip
83 def __call__(self, value):
84 if value is None or value.strip(' ') == '':
85 message = _("Invalid configuration. hpe3par_fpg must be set.")
86 LOG.error(message)
87 raise exception.HPE3ParInvalid(err=message)
89 ips = []
90 values = value.split(",")
91 # Extract pool name
92 pool_name = values.pop(0).strip()
94 # values will now be ['ip1', ...]
95 if len(values) < self.min_ip:
96 msg = (_("Require at least %s IPs configured per "
97 "pool") % self.min_ip)
98 raise exception.HPE3ParInvalid(err=msg)
99 if len(values) > self.max_ip:
100 msg = (_("Cannot configure IPs more than max supported "
101 "%s IPs per pool") % self.max_ip)
102 raise exception.HPE3ParInvalid(err=msg)
104 for ip_addr in values:
105 ip_addr = types.String.__call__(self, ip_addr.strip())
106 try:
107 ips.append(types.IPAddress.__call__(self, ip_addr))
108 except ValueError as verror:
109 raise exception.HPE3ParInvalid(err=verror)
110 fpg = {pool_name: ips}
111 return fpg
113 def __repr__(self):
114 return 'FPG'
116 def _formatter(self, value):
117 return str(value)
120HPE3PAR_OPTS = [
121 cfg.StrOpt('hpe3par_api_url',
122 default='',
123 help="3PAR WSAPI Server Url like "
124 "https://<3par ip>:8080/api/v1"),
125 cfg.StrOpt('hpe3par_username',
126 default='',
127 help="3PAR username with the 'edit' role"),
128 cfg.StrOpt('hpe3par_password',
129 default='',
130 help="3PAR password for the user specified in hpe3par_username",
131 secret=True),
132 cfg.HostAddressOpt('hpe3par_san_ip',
133 help="IP address of SAN controller"),
134 cfg.StrOpt('hpe3par_san_login',
135 default='',
136 help="Username for SAN controller"),
137 cfg.StrOpt('hpe3par_san_password',
138 default='',
139 help="Password for SAN controller",
140 secret=True),
141 cfg.PortOpt('hpe3par_san_ssh_port',
142 default=22,
143 help='SSH port to use with SAN'),
144 cfg.MultiOpt('hpe3par_fpg',
145 item_type=FPG(min_ip=0, max_ip=FPG.MAX_SUPPORTED_IP_PER_VFS),
146 help="The File Provisioning Group (FPG) to use"),
147 cfg.BoolOpt('hpe3par_fstore_per_share',
148 default=False,
149 help="Use one filestore per share"),
150 cfg.BoolOpt('hpe3par_require_cifs_ip',
151 default=False,
152 help="Require IP access rules for CIFS (in addition to user)"),
153 cfg.BoolOpt('hpe3par_debug',
154 default=False,
155 help="Enable HTTP debugging to 3PAR"),
156 cfg.StrOpt('hpe3par_cifs_admin_access_username',
157 default='',
158 help="File system admin user name for CIFS."),
159 cfg.StrOpt('hpe3par_cifs_admin_access_password',
160 default='',
161 help="File system admin password for CIFS.",
162 secret=True),
163 cfg.StrOpt('hpe3par_cifs_admin_access_domain',
164 default='LOCAL_CLUSTER',
165 help="File system domain for the CIFS admin user."),
166 cfg.StrOpt('hpe3par_share_mount_path',
167 default='/mnt/',
168 help="The path where shares will be mounted when deleting "
169 "nested file trees."),
170]
172CONF = cfg.CONF
173CONF.register_opts(HPE3PAR_OPTS)
176def to_list(var):
177 """Convert var to list type if not"""
178 if isinstance(var, str): 178 ↛ 181line 178 didn't jump to line 181 because the condition on line 178 was always true
179 return [var]
180 else:
181 return var
184class HPE3ParShareDriver(driver.ShareDriver):
185 """HPE 3PAR driver for Manila.
187 Supports NFS and CIFS protocols on arrays with File Persona.
189 Version history::
191 1.0.0 - Begin Liberty development (post-Kilo)
192 1.0.1 - Report thin/dedup/hp_flash_cache capabilities
193 1.0.2 - Add share server/share network support
194 2.0.0 - Rebranded HP to HPE
195 2.0.1 - Add access_level (e.g. read-only support)
196 2.0.2 - Add extend/shrink
197 2.0.3 - Remove file tree on delete when using nested shares #1538800
198 2.0.4 - Reduce the fsquota by share size
199 when a share is deleted #1582931
200 2.0.5 - Add update_access support
201 2.0.6 - Multi pool support per backend
202 2.0.7 - Fix get_vfs() to correctly validate conf IP addresses at
203 boot up #1621016
204 2.0.8 - Replace ConsistencyGroup with ShareGroup
206 """
208 VERSION = "2.0.8"
210 def __init__(self, *args, **kwargs):
211 super(HPE3ParShareDriver, self).__init__((True, False),
212 *args,
213 **kwargs)
215 self.configuration = kwargs.get('configuration', None)
216 self.configuration.append_config_values(HPE3PAR_OPTS)
217 self.configuration.append_config_values(driver.ssh_opts)
218 self.configuration.append_config_values(config.global_opts)
219 self.fpgs = {}
220 self._hpe3par = None # mediator between driver and client
222 def do_setup(self, context):
223 """Any initialization the share driver does while starting."""
225 LOG.info("Starting share driver %(driver_name)s (%(version)s)",
226 {'driver_name': self.__class__.__name__,
227 'version': self.VERSION})
229 mediator = hpe_3par_mediator.HPE3ParMediator(
230 hpe3par_username=self.configuration.hpe3par_username,
231 hpe3par_password=self.configuration.hpe3par_password,
232 hpe3par_api_url=self.configuration.hpe3par_api_url,
233 hpe3par_debug=self.configuration.hpe3par_debug,
234 hpe3par_san_ip=self.configuration.hpe3par_san_ip,
235 hpe3par_san_login=self.configuration.hpe3par_san_login,
236 hpe3par_san_password=self.configuration.hpe3par_san_password,
237 hpe3par_san_ssh_port=self.configuration.hpe3par_san_ssh_port,
238 hpe3par_fstore_per_share=(self.configuration
239 .hpe3par_fstore_per_share),
240 hpe3par_require_cifs_ip=self.configuration.hpe3par_require_cifs_ip,
241 hpe3par_cifs_admin_access_username=(
242 self.configuration.hpe3par_cifs_admin_access_username),
243 hpe3par_cifs_admin_access_password=(
244 self.configuration.hpe3par_cifs_admin_access_password),
245 hpe3par_cifs_admin_access_domain=(
246 self.configuration.hpe3par_cifs_admin_access_domain),
247 hpe3par_share_mount_path=(
248 self.configuration.hpe3par_share_mount_path),
249 my_ip=self.configuration.my_ip,
250 ssh_conn_timeout=self.configuration.ssh_conn_timeout,
251 )
253 mediator.do_setup()
255 def _validate_pool_ips(addresses, conf_pool_ips):
256 # Pool configured IP addresses should be subset of IP addresses
257 # retured from vfs
258 if not set(conf_pool_ips) <= set(addresses):
259 msg = _("Incorrect configuration. "
260 "Configuration pool IP address did not match with "
261 "IP addresses at 3par array")
262 raise exception.HPE3ParInvalid(err=msg)
264 def _construct_fpg():
265 # FPG must be configured and must exist.
266 # self.configuration.safe_get('hpe3par_fpg') will have value in
267 # following format:
268 # [ {'pool_name':['ip_addr', 'ip_addr', ...]}, ... ]
269 for fpg in self.configuration.safe_get('hpe3par_fpg'):
270 pool_name = list(fpg)[0]
271 conf_pool_ips = fpg[pool_name]
273 # Validate the FPG and discover the VFS
274 # This also validates the client, connection, firmware, WSAPI,
275 # FPG...
276 vfs_info = mediator.get_vfs(pool_name)
277 if self.driver_handles_share_servers:
278 # Use discovered IP(s) from array
279 self.fpgs[pool_name] = {
280 vfs_info['vfsname']: vfs_info['vfsip']['address']}
281 elif conf_pool_ips == []:
282 # not DHSS and IPs not configured in manila.conf.
283 if not vfs_info['vfsip']['address']:
284 msg = _("Unsupported configuration. "
285 "hpe3par_fpg must have IP address "
286 "or be discoverable at 3PAR")
287 LOG.error(msg)
288 raise exception.HPE3ParInvalid(err=msg)
289 else:
290 # Use discovered pool ips
291 self.fpgs[pool_name] = {
292 vfs_info['vfsname']: vfs_info['vfsip']['address']}
293 else:
294 # not DHSS and IPs configured in manila.conf
295 _validate_pool_ips(vfs_info['vfsip']['address'],
296 conf_pool_ips)
297 self.fpgs[pool_name] = {
298 vfs_info['vfsname']: conf_pool_ips}
300 _construct_fpg()
302 # Don't set _hpe3par until it is ready. Otherwise _update_stats fails.
303 self._hpe3par = mediator
305 def _get_pool_location_from_share_host(self, share_instance_host):
306 # Return pool name, vfs, IPs for a pool from share instance host
307 pool_name = share_utils.extract_host(share_instance_host, level='pool')
308 if not pool_name:
309 message = (_("Pool is not available in the share host %s.") %
310 share_instance_host)
311 raise exception.InvalidHost(reason=message)
313 if pool_name not in self.fpgs:
314 message = (_("Pool location lookup failed. "
315 "Could not find pool %s") %
316 pool_name)
317 raise exception.InvalidHost(reason=message)
319 vfs = list(self.fpgs[pool_name])[0]
320 ips = self.fpgs[pool_name][vfs]
322 return (pool_name, vfs, ips)
324 def _get_pool_location(self, share, share_server=None):
325 # Return pool name, vfs, IPs for a pool from share host field
326 # Use share_server if provided, instead of self.fpgs
327 if share_server is not None:
328 # When DHSS
329 ips = share_server['backend_details'].get('ip')
330 ips = to_list(ips)
331 vfs = share_server['backend_details'].get('vfs')
332 pool_name = share_server['backend_details'].get('fpg')
333 return (pool_name, vfs, ips)
334 else:
335 # When DHSS = false
336 return self._get_pool_location_from_share_host(share['host'])
338 def check_for_setup_error(self):
340 try:
341 # Log the source SHA for support. Only do this with DEBUG.
342 if LOG.isEnabledFor(log.DEBUG): 342 ↛ exitline 342 didn't return from function 'check_for_setup_error' because the condition on line 342 was always true
343 LOG.debug('HPE3ParShareDriver SHA1: %s',
344 self.sha1_hash(HPE3ParShareDriver))
345 LOG.debug('HPE3ParMediator SHA1: %s',
346 self.sha1_hash(hpe_3par_mediator.HPE3ParMediator))
347 except Exception as e:
348 # Don't let any exceptions during the SHA1 logging interfere
349 # with startup. This is just debug info to identify the source
350 # code. If it doesn't work, just log a debug message.
351 LOG.debug('Source code SHA1 not logged due to: %s',
352 str(e))
354 @staticmethod
355 def sha1_hash(clazz):
356 """Get the SHA1 hash for the source of a class."""
357 source_file = inspect.getsourcefile(clazz)
358 file_size = os.path.getsize(source_file)
360 sha1 = hashlib.sha1(usedforsecurity=False)
361 sha1.update(("blob %u\0" % file_size).encode('utf-8'))
363 with open(source_file, 'rb') as f:
364 sha1.update(f.read())
366 return sha1.hexdigest()
368 def get_network_allocations_number(self):
369 return 1
371 def choose_share_server_compatible_with_share(self, context, share_servers,
372 share, snapshot=None,
373 share_group=None,
374 encryption_key_ref=None):
375 """Method that allows driver to choose share server for provided share.
377 If compatible share-server is not found, method should return None.
379 :param context: Current context
380 :param share_servers: list with share-server models
381 :param share: share model
382 :param snapshot: snapshot model
383 :param share_group: ShareGroup model with shares
384 :returns: share-server or None
385 """
386 # If creating in a share group, raise exception
387 if share_group:
388 msg = _("HPE 3PAR driver does not support share group")
389 raise exception.InvalidRequest(message=msg)
391 pool_name = share_utils.extract_host(share['host'], level='pool')
392 for share_server in share_servers:
393 if share_server['backend_details'].get('fpg') == pool_name: 393 ↛ 392line 393 didn't jump to line 392 because the condition on line 393 was always true
394 return share_server
395 return None
397 @staticmethod
398 def _validate_network_type(network_type):
399 if network_type not in ('flat', 'vlan', None):
400 reason = _('Invalid network type. %s is not supported by the '
401 '3PAR driver.')
402 raise exception.NetworkBadConfigurationException(
403 reason=reason % network_type)
405 def _create_share_server(self, network_info, request_host=None):
406 """Is called to create/setup share server"""
407 # Return pool name, vfs, IPs for a pool
408 pool_name, vfs, ips = self._get_pool_location_from_share_host(
409 request_host)
411 ip = network_info['network_allocations'][0]['ip_address']
412 if ip not in ips: 412 ↛ 427line 412 didn't jump to line 427 because the condition on line 412 was always true
413 # Besides DHSS, admin could have setup IP to VFS directly on array
414 if len(ips) > (FPG.MAX_SUPPORTED_IP_PER_VFS - 1):
415 message = (_("Pool %s has exceeded 3PAR's "
416 "max supported VFS IP address") % pool_name)
417 LOG.error(message)
418 raise exception.Invalid(message)
420 subnet = utils.cidr_to_netmask(network_info['cidr'])
421 vlantag = network_info['segmentation_id']
423 self._hpe3par.create_fsip(ip, subnet, vlantag, pool_name, vfs)
424 # Update in global saved config, self.fpgs[pool_name]
425 ips.append(ip)
427 return {'share_server_name': network_info['server_id'],
428 'share_server_id': network_info['server_id'],
429 'ip': ip,
430 'subnet': subnet,
431 'vlantag': vlantag if vlantag else 0,
432 'fpg': pool_name,
433 'vfs': vfs}
435 def _setup_server(self, network_info, metadata=None):
436 # NOTE(felipe_rodrigues): keep legacy network_info support as a dict.
437 network_info = network_info[0]
439 LOG.debug("begin _setup_server with %s", network_info)
441 self._validate_network_type(network_info['network_type'])
442 if metadata is not None and metadata['request_host'] is not None: 442 ↛ exitline 442 didn't return from function '_setup_server' because the condition on line 442 was always true
443 return self._create_share_server(network_info,
444 metadata['request_host'])
446 def _teardown_server(self, server_details, security_services=None):
447 LOG.debug("begin _teardown_server with %s", server_details)
448 fpg = server_details.get('fpg')
449 vfs = server_details.get('vfs')
450 ip = server_details.get('ip')
451 self._hpe3par.remove_fsip(ip, fpg, vfs)
452 if ip in self.fpgs[fpg][vfs]: 452 ↛ exitline 452 didn't return from function '_teardown_server' because the condition on line 452 was always true
453 self.fpgs[fpg][vfs].remove(ip)
455 @staticmethod
456 def build_share_comment(share):
457 """Create an informational only comment to help admins and testers."""
459 info = {
460 'name': share['display_name'],
461 'host': share['host'],
462 'now': datetime.datetime.now().strftime('%H%M%S'),
463 }
465 acceptable = re.compile(r'[^a-zA-Z0-9_=:@# \-]+', re.UNICODE)
466 comment = ("OpenStack Manila - host=%(host)s orig_name=%(name)s "
467 "created=%(now)s" % info)
469 return acceptable.sub('_', comment)[:254] # clean and truncate
471 def create_share(self, context, share, share_server=None):
472 """Is called to create share."""
474 fpg, vfs, ips = self._get_pool_location(share, share_server)
476 protocol = share['share_proto']
477 extra_specs = share_types.get_extra_specs_from_share(share)
479 path = self._hpe3par.create_share(
480 share['project_id'],
481 share['id'],
482 protocol,
483 extra_specs,
484 fpg, vfs,
485 size=share['size'],
486 comment=self.build_share_comment(share)
487 )
489 return self._hpe3par.build_export_locations(protocol, ips, path)
491 def create_share_from_snapshot(self, context, share, snapshot,
492 share_server=None, parent_share=None):
493 """Is called to create share from snapshot."""
495 fpg, vfs, ips = self._get_pool_location(share, share_server)
497 protocol = share['share_proto']
498 extra_specs = share_types.get_extra_specs_from_share(share)
500 path = self._hpe3par.create_share_from_snapshot(
501 share['id'],
502 protocol,
503 extra_specs,
504 share['project_id'],
505 snapshot['share_id'],
506 snapshot['id'],
507 fpg,
508 vfs,
509 ips,
510 size=share['size'],
511 comment=self.build_share_comment(share)
512 )
514 return self._hpe3par.build_export_locations(protocol, ips, path)
516 def delete_share(self, context, share, share_server=None):
517 """Deletes share and its fstore."""
519 fpg, vfs, ips = self._get_pool_location(share, share_server)
520 self._hpe3par.delete_share(share['project_id'],
521 share['id'],
522 share['size'],
523 share['share_proto'],
524 fpg,
525 vfs,
526 ips[0])
528 def create_snapshot(self, context, snapshot, share_server=None):
529 """Creates a snapshot of a share."""
531 fpg, vfs, ips = self._get_pool_location(snapshot['share'],
532 share_server)
533 self._hpe3par.create_snapshot(snapshot['share']['project_id'],
534 snapshot['share']['id'],
535 snapshot['share']['share_proto'],
536 snapshot['id'],
537 fpg,
538 vfs)
540 def delete_snapshot(self, context, snapshot, share_server=None):
541 """Deletes a snapshot of a share."""
543 fpg, vfs, ips = self._get_pool_location(snapshot['share'],
544 share_server)
545 self._hpe3par.delete_snapshot(snapshot['share']['project_id'],
546 snapshot['share']['id'],
547 snapshot['share']['share_proto'],
548 snapshot['id'],
549 fpg,
550 vfs)
552 def ensure_share(self, context, share, share_server=None):
553 pass
555 def update_access(self, context, share, access_rules, add_rules,
556 delete_rules, update_rules, share_server=None):
557 """Update access to the share."""
558 extra_specs = None
559 if 'NFS' == share['share_proto']: # Avoiding DB call otherwise 559 ↛ 562line 559 didn't jump to line 562 because the condition on line 559 was always true
560 extra_specs = share_types.get_extra_specs_from_share(share)
562 fpg, vfs, ips = self._get_pool_location(share, share_server)
563 self._hpe3par.update_access(share['project_id'],
564 share['id'],
565 share['share_proto'],
566 extra_specs,
567 access_rules,
568 add_rules,
569 delete_rules,
570 fpg,
571 vfs)
573 def extend_share(self, share, new_size, share_server=None):
574 """Extends size of existing share."""
576 fpg, vfs, ips = self._get_pool_location(share, share_server)
577 self._hpe3par.resize_share(share['project_id'],
578 share['id'],
579 share['share_proto'],
580 new_size,
581 share['size'],
582 fpg,
583 vfs)
585 def shrink_share(self, share, new_size, share_server=None):
586 """Shrinks size of existing share."""
588 fpg, vfs, ips = self._get_pool_location(share, share_server)
589 self._hpe3par.resize_share(share['project_id'],
590 share['id'],
591 share['share_proto'],
592 new_size,
593 share['size'],
594 fpg,
595 vfs)
597 def _update_share_stats(self):
598 """Retrieve stats info from share group."""
600 backend_name = self.configuration.safe_get(
601 'share_backend_name') or "HPE_3PAR"
603 max_over_subscription_ratio = self.configuration.safe_get(
604 'max_over_subscription_ratio')
606 reserved_share_percentage = self.configuration.safe_get(
607 'reserved_share_percentage')
608 if reserved_share_percentage is None: 608 ↛ 611line 608 didn't jump to line 611 because the condition on line 608 was always true
609 reserved_share_percentage = 0
611 reserved_share_from_snapshot_percentage = self.configuration.safe_get(
612 'reserved_share_from_snapshot_percentage')
613 if reserved_share_from_snapshot_percentage is None: 613 ↛ 616line 613 didn't jump to line 616 because the condition on line 613 was always true
614 reserved_share_from_snapshot_percentage = reserved_share_percentage
616 reserved_share_extend_percentage = self.configuration.safe_get(
617 'reserved_share_extend_percentage')
618 if reserved_share_extend_percentage is None: 618 ↛ 621line 618 didn't jump to line 621 because the condition on line 618 was always true
619 reserved_share_extend_percentage = reserved_share_percentage
621 stats = {
622 'share_backend_name': backend_name,
623 'driver_handles_share_servers': self.driver_handles_share_servers,
624 'vendor_name': 'HPE',
625 'driver_version': self.VERSION,
626 'storage_protocol': 'NFS_CIFS',
627 'total_capacity_gb': 0,
628 'free_capacity_gb': 0,
629 'provisioned_capacity_gb': 0,
630 'reserved_percentage': reserved_share_percentage,
631 'reserved_snapshot_percentage':
632 reserved_share_from_snapshot_percentage,
633 'reserved_share_extend_percentage':
634 reserved_share_extend_percentage,
635 'max_over_subscription_ratio': max_over_subscription_ratio,
636 'qos': False,
637 'thin_provisioning': True, # 3PAR default is thin
638 }
640 if not self._hpe3par:
641 LOG.info(
642 "Skipping capacity and capabilities update. Setup has not "
643 "completed.")
644 else:
645 for fpg in self.fpgs:
646 fpg_status = self._hpe3par.get_fpg_status(fpg)
647 fpg_status['reserved_percentage'] = reserved_share_percentage
648 fpg_status['reserved_snapshot_percentage'] = (
649 reserved_share_from_snapshot_percentage)
650 fpg_status['reserved_share_extend_percentage'] = (
651 reserved_share_extend_percentage)
652 LOG.debug("FPG status = %s.", fpg_status)
653 stats.setdefault('pools', []).append(fpg_status)
655 super(HPE3ParShareDriver, self)._update_share_stats(stats)