Coverage for manila/share/drivers/helpers.py: 96%

311 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2015 Mirantis 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 

16import copy 

17import ipaddress 

18import os 

19import re 

20 

21from oslo_log import log 

22 

23from manila.common import constants as const 

24from manila import exception 

25from manila.i18n import _ 

26from manila import utils 

27 

28LOG = log.getLogger(__name__) 

29 

30 

31class NASHelperBase(object): 

32 """Interface to work with share.""" 

33 

34 def __init__(self, execute, ssh_execute, config_object): 

35 self.configuration = config_object 

36 self._execute = execute 

37 self._ssh_exec = ssh_execute 

38 

39 def init_helper(self, server): 

40 pass 

41 

42 def create_exports(self, server, share_name, recreate=False): 

43 """Create new exports, delete old ones if exist.""" 

44 raise NotImplementedError() 

45 

46 def remove_exports(self, server, share_name): 

47 """Remove exports.""" 

48 raise NotImplementedError() 

49 

50 def configure_access(self, server, share_name): 

51 """Configure server before allowing access.""" 

52 pass 

53 

54 def update_access(self, server, share_name, access_rules, add_rules, 

55 delete_rules): 

56 """Update access rules for given share. 

57 

58 This driver has two different behaviors according to parameters: 

59 1. Recovery after error - 'access_rules' contains all access_rules, 

60 'add_rules' and 'delete_rules' shall be empty. Previously existing 

61 access rules are cleared and then added back according 

62 to 'access_rules'. 

63 

64 2. Adding/Deleting of several access rules - 'access_rules' contains 

65 all access_rules, 'add_rules' and 'delete_rules' contain rules which 

66 should be added/deleted. Rules in 'access_rules' are ignored and 

67 only rules from 'add_rules' and 'delete_rules' are applied. 

68 

69 :param server: None or Share server's backend details 

70 :param share_name: Share's path according to id. 

71 :param access_rules: All access rules for given share 

72 :param add_rules: Empty List or List of access rules which should be 

73 added. access_rules already contains these rules. 

74 :param delete_rules: Empty List or List of access rules which should be 

75 removed. access_rules doesn't contain these rules. 

76 """ 

77 raise NotImplementedError() 

78 

79 @staticmethod 

80 def _verify_server_has_public_address(server): 

81 if 'public_address' in server: 

82 pass 

83 elif 'public_addresses' in server: 

84 if not isinstance(server['public_addresses'], list): 84 ↛ 85line 84 didn't jump to line 85 because the condition on line 84 was never true

85 raise exception.ManilaException(_("public_addresses must be " 

86 "a list")) 

87 else: 

88 raise exception.ManilaException( 

89 _("Can not get public_address(es) for generation of export.")) 

90 

91 def _get_export_location_template(self, export_location_or_path): 

92 """Returns template of export location. 

93 

94 Example for NFS: 

95 %s:/path/to/share 

96 Example for CIFS: 

97 \\\\%s\\cifs_share_name 

98 """ 

99 raise NotImplementedError() 

100 

101 def get_exports_for_share(self, server, export_location_or_path): 

102 """Returns list of exports based on server info.""" 

103 self._verify_server_has_public_address(server) 

104 export_location_template = self._get_export_location_template( 

105 export_location_or_path) 

106 export_locations = [] 

107 

108 if 'public_addresses' in server: 

109 pairs = list(map(lambda addr: (addr, False), 

110 server['public_addresses'])) 

111 else: 

112 pairs = [(server['public_address'], False)] 

113 

114 # NOTE(vponomaryov): 

115 # Generic driver case: 'admin_ip' exists only in case of DHSS=True 

116 # mode and 'ip' exists in case of DHSS=False mode. 

117 # Use one of these for creation of export location for service needs. 

118 service_address = server.get("admin_ip", server.get("ip")) 

119 if service_address: 

120 pairs.append((service_address, True)) 

121 for ip, is_admin in pairs: 

122 export_locations.append({ 

123 "path": export_location_template % ip, 

124 "is_admin_only": is_admin, 

125 "metadata": { 

126 # TODO(vponomaryov): remove this fake metadata when 

127 # proper appears. 

128 "export_location_metadata_example": "example", 

129 }, 

130 }) 

131 return export_locations 

132 

133 def get_share_path_by_export_location(self, server, export_location): 

134 """Returns share path by its export location.""" 

135 raise NotImplementedError() 

136 

137 def disable_access_for_maintenance(self, server, share_name): 

138 """Disables access to share to perform maintenance operations.""" 

139 

140 def restore_access_after_maintenance(self, server, share_name): 

141 """Enables access to share after maintenance operations were done.""" 

142 

143 @staticmethod 

144 def validate_access_rules(access_rules, allowed_types, allowed_levels): 

145 """Validates access rules according to access_type and access_level. 

146 

147 :param access_rules: List of access rules to be validated. 

148 :param allowed_types: tuple of allowed type values. 

149 :param allowed_levels: tuple of allowed level values. 

150 """ 

151 for access in (access_rules or []): 

152 access_type = access['access_type'] 

153 access_level = access['access_level'] 

154 if access_type not in allowed_types: 

155 reason = _("Only %s access type allowed.") % ( 

156 ', '.join(tuple(["'%s'" % x for x in allowed_types]))) 

157 raise exception.InvalidShareAccess(reason=reason) 

158 if access_level not in allowed_levels: 

159 raise exception.InvalidShareAccessLevel(level=access_level) 

160 

161 def _get_maintenance_file_path(self, share_name): 

162 return os.path.join(self.configuration.share_mount_path, 

163 "%s.maintenance" % share_name) 

164 

165 

166def nfs_synchronized(f): 

167 

168 def wrapped_func(self, *args, **kwargs): 

169 key = "nfs-%s" % args[0].get("lock_name", args[0]["instance_id"]) 

170 

171 # NOTE(vponomaryov): 'external' lock is required for DHSS=False 

172 # mode of LVM and Generic drivers, that may have lots of 

173 # driver instances on single host. 

174 @utils.synchronized(key, external=True) 

175 def source_func(self, *args, **kwargs): 

176 return f(self, *args, **kwargs) 

177 

178 return source_func(self, *args, **kwargs) 

179 

180 return wrapped_func 

181 

182 

183def escaped_address(address): 

184 addr = ipaddress.ip_address(str(address)) 

185 if addr.version == 4: 

186 return str(addr) 

187 else: 

188 return '[%s]' % addr 

189 

190 

191class NFSHelper(NASHelperBase): 

192 """Interface to work with share.""" 

193 

194 def create_exports(self, server, share_name, recreate=False): 

195 path = os.path.join(self.configuration.share_mount_path, share_name) 

196 server_copy = copy.copy(server) 

197 public_addresses = [] 

198 if 'public_addresses' in server_copy: 

199 for address in server_copy['public_addresses']: 

200 public_addresses.append( 

201 escaped_address(address)) 

202 server_copy['public_addresses'] = public_addresses 

203 

204 for t in ['public_address', 'admin_ip', 'ip']: 

205 address = server_copy.get(t) 

206 if address is not None: 

207 server_copy[t] = escaped_address(address) 

208 

209 return self.get_exports_for_share(server_copy, path) 

210 

211 def init_helper(self, server): 

212 try: 

213 self._ssh_exec(server, ['sudo', 'exportfs']) 

214 except exception.ProcessExecutionError as e: 

215 if 'command not found' in e.stderr: 

216 raise exception.ManilaException( 

217 _('NFS server is not installed on %s') 

218 % server['instance_id']) 

219 LOG.error(e.stderr) 

220 

221 def remove_exports(self, server, share_name): 

222 """Remove exports.""" 

223 

224 @nfs_synchronized 

225 def update_access(self, server, share_name, access_rules, add_rules, 

226 delete_rules): 

227 """Update access rules for given share. 

228 

229 Please refer to base class for a more in-depth description. 

230 """ 

231 local_path = os.path.join(self.configuration.share_mount_path, 

232 share_name) 

233 out, err = self._ssh_exec(server, ['sudo', 'exportfs']) 

234 # Recovery mode 

235 if not (add_rules or delete_rules): 

236 

237 self.validate_access_rules( 

238 access_rules, ('ip',), 

239 (const.ACCESS_LEVEL_RO, const.ACCESS_LEVEL_RW)) 

240 

241 hosts = self.get_host_list(out, local_path) 

242 for host in hosts: 

243 parsed_host = self._get_parsed_address_or_cidr(host) 

244 self._ssh_exec(server, ['sudo', 'exportfs', '-u', 

245 ':'.join((parsed_host, local_path))]) 

246 self._sync_nfs_temp_and_perm_files(server) 

247 for access in access_rules: 

248 rules_options = '%s,no_subtree_check,no_root_squash' 

249 access_to = self._get_parsed_address_or_cidr( 

250 access['access_to']) 

251 self._ssh_exec( 

252 server, 

253 ['sudo', 'exportfs', '-o', 

254 rules_options % access['access_level'], 

255 ':'.join((access_to, local_path))]) 

256 self._sync_nfs_temp_and_perm_files(server) 

257 # Adding/Deleting specific rules 

258 else: 

259 

260 self.validate_access_rules( 

261 add_rules, ('ip',), 

262 (const.ACCESS_LEVEL_RO, const.ACCESS_LEVEL_RW)) 

263 

264 for access in delete_rules: 

265 try: 

266 self.validate_access_rules( 

267 [access], ('ip',), 

268 (const.ACCESS_LEVEL_RO, const.ACCESS_LEVEL_RW)) 

269 except (exception.InvalidShareAccess, 

270 exception.InvalidShareAccessLevel): 

271 LOG.warning( 

272 "Unsupported access level %(level)s or access type " 

273 "%(type)s, skipping removal of access rule to " 

274 "%(to)s.", {'level': access['access_level'], 

275 'type': access['access_type'], 

276 'to': access['access_to']}) 

277 continue 

278 access_to = self._get_parsed_address_or_cidr( 

279 access['access_to']) 

280 try: 

281 self._ssh_exec(server, ['sudo', 'exportfs', '-u', 

282 ':'.join((access_to, local_path))]) 

283 except exception.ProcessExecutionError as e: 

284 if "could not find" in e.stderr.lower(): 284 ↛ 290line 284 didn't jump to line 290 because the condition on line 284 was always true

285 LOG.debug( 

286 "Client/s with IP address/es %(host)s did not " 

287 "have access to %(share)s. Nothing to deny.", 

288 {'host': access_to, 'share': share_name}) 

289 else: 

290 raise 

291 

292 if delete_rules: 292 ↛ 294line 292 didn't jump to line 294 because the condition on line 292 was always true

293 self._sync_nfs_temp_and_perm_files(server) 

294 for access in add_rules: 

295 access_to = self._get_parsed_address_or_cidr( 

296 access['access_to']) 

297 found_item = re.search( 

298 re.escape(local_path) + r'[\s\n]*' + re.escape(access_to), 

299 out) 

300 if found_item is not None: 

301 LOG.warning("Access rule %(type)s:%(to)s already " 

302 "exists for share %(name)s", { 

303 'to': access['access_to'], 

304 'type': access['access_type'], 

305 'name': share_name 

306 }) 

307 else: 

308 rules_options = '%s,no_subtree_check,no_root_squash' 

309 self._ssh_exec( 

310 server, 

311 ['sudo', 'exportfs', '-o', 

312 rules_options % access['access_level'], 

313 ':'.join((access_to, local_path))]) 

314 if add_rules: 

315 self._sync_nfs_temp_and_perm_files(server) 

316 

317 @staticmethod 

318 def _get_parsed_address_or_cidr(access_to): 

319 network = ipaddress.ip_network(str(access_to)) 

320 mask_length = network.prefixlen 

321 address = str(network.network_address) 

322 if mask_length == 0: 

323 # Special case because Linux exports don't support /0 netmasks 

324 return '*' 

325 if network.version == 4: 

326 if mask_length == 32: 

327 return address 

328 return '%s/%s' % (address, mask_length) 

329 if mask_length == 128: 

330 return "[%s]" % address 

331 return "[%s]/%s" % (address, mask_length) 

332 

333 @staticmethod 

334 def get_host_list(output, local_path): 

335 entries = [] 

336 output = output.replace('\n\t\t', ' ') 

337 lines = output.split('\n') 

338 for line in lines: 

339 items = line.split(' ') 

340 if local_path == items[0]: 

341 entries.append(items[1]) 

342 # exportfs may print"<world>" instead of "*" for host 

343 entries = ["*" if item == "<world>" else item for item in entries] 

344 return entries 

345 

346 def _sync_nfs_temp_and_perm_files(self, server): 

347 """Sync changes of exports with permanent NFS config file. 

348 

349 This is required to ensure, that after share server reboot, exports 

350 still exist. 

351 """ 

352 sync_cmd = [ 

353 'sudo', 'cp', const.NFS_EXPORTS_FILE_TEMP, const.NFS_EXPORTS_FILE 

354 ] 

355 self._ssh_exec(server, sync_cmd) 

356 self._ssh_exec(server, ['sudo', 'exportfs', '-a']) 

357 out, _ = self._ssh_exec( 

358 server, 

359 ['sudo', 'systemctl', 'is-active', 'nfs-kernel-server'], 

360 check_exit_code=False) 

361 if "inactive" in out: 361 ↛ 362line 361 didn't jump to line 362 because the condition on line 361 was never true

362 self._ssh_exec( 

363 server, ['sudo', 'systemctl', 'restart', 'nfs-kernel-server']) 

364 

365 def _get_export_location_template(self, export_location_or_path): 

366 path = export_location_or_path.split(':')[-1] 

367 return '%s:' + path 

368 

369 def get_share_path_by_export_location(self, server, export_location): 

370 return export_location.split(':')[-1] 

371 

372 @nfs_synchronized 

373 def disable_access_for_maintenance(self, server, share_name): 

374 maintenance_file = self._get_maintenance_file_path(share_name) 

375 backup_exports = [ 

376 'cat', const.NFS_EXPORTS_FILE, 

377 '|', 'grep', share_name, 

378 '|', 'sudo', 'tee', maintenance_file 

379 ] 

380 self._ssh_exec(server, backup_exports) 

381 

382 local_path = os.path.join(self.configuration.share_mount_path, 

383 share_name) 

384 out, err = self._ssh_exec(server, ['sudo', 'exportfs']) 

385 hosts = self.get_host_list(out, local_path) 

386 for host in hosts: 

387 self._ssh_exec(server, 

388 ['sudo', 'exportfs', '-u', 

389 '"{}"'.format(':'.join((host, local_path)))]) 

390 self._sync_nfs_temp_and_perm_files(server) 

391 

392 @nfs_synchronized 

393 def restore_access_after_maintenance(self, server, share_name): 

394 maintenance_file = self._get_maintenance_file_path(share_name) 

395 restore_exports = [ 

396 'cat', maintenance_file, 

397 '|', 'sudo', 'tee', '-a', const.NFS_EXPORTS_FILE, 

398 '&&', 'sudo', 'exportfs', '-r', 

399 '&&', 'sudo', 'rm', '-f', maintenance_file 

400 ] 

401 self._ssh_exec(server, restore_exports) 

402 

403 

404class CIFSHelperBase(NASHelperBase): 

405 @staticmethod 

406 def _get_share_group_name_from_export_location(export_location): 

407 if '/' in export_location and '\\' in export_location: 

408 pass 

409 elif export_location.startswith('\\\\'): 

410 return export_location.split('\\')[-1] 

411 elif export_location.startswith('//'): 

412 return export_location.split('/')[-1] 

413 

414 msg = _("Got incorrect CIFS export location '%s'.") % export_location 

415 raise exception.InvalidShare(reason=msg) 

416 

417 def _get_export_location_template(self, export_location_or_path): 

418 group_name = self._get_share_group_name_from_export_location( 

419 export_location_or_path) 

420 return ('\\\\%s' + ('\\%s' % group_name)) 

421 

422 

423class CIFSHelperIPAccess(CIFSHelperBase): 

424 """Manage shares in samba server by net conf tool. 

425 

426 Class provides functionality to operate with CIFS shares. 

427 Samba server should be configured to use registry as configuration 

428 backend to allow dynamically share managements. This class allows 

429 to define access to shares by IPs with RW access level. 

430 """ 

431 def __init__(self, *args): 

432 super(CIFSHelperIPAccess, self).__init__(*args) 

433 self.parameters = { 

434 'browseable': 'yes', 

435 'create mask': '0755', 

436 'hosts deny': '0.0.0.0/0', # deny all by default 

437 'hosts allow': '127.0.0.1', 

438 'read only': 'no', 

439 } 

440 

441 def init_helper(self, server): 

442 # This is smoke check that we have required dependency 

443 self._ssh_exec(server, ['sudo', 'net', 'conf', 'list']) 

444 

445 def create_exports(self, server, share_name, recreate=False): 

446 """Create share at samba server.""" 

447 share_path = os.path.join(self.configuration.share_mount_path, 

448 share_name) 

449 create_cmd = [ 

450 'sudo', 'net', 'conf', 'addshare', share_name, share_path, 

451 'writeable=y', 'guest_ok=y', 

452 ] 

453 try: 

454 self._ssh_exec( 

455 server, ['sudo', 'net', 'conf', 'showshare', share_name, ]) 

456 except exception.ProcessExecutionError: 

457 # Share does not exist, create it 

458 try: 

459 self._ssh_exec(server, create_cmd) 

460 except Exception: 

461 msg = _("Could not create CIFS export %s.") % share_name 

462 LOG.exception(msg) 

463 raise exception.ManilaException(msg) 

464 else: 

465 # Share exists 

466 if recreate: 

467 self._ssh_exec( 

468 server, ['sudo', 'net', 'conf', 'delshare', share_name, ]) 

469 try: 

470 self._ssh_exec(server, create_cmd) 

471 except Exception: 

472 msg = _("Could not create CIFS export %s.") % share_name 

473 LOG.exception(msg) 

474 raise exception.ManilaException(msg) 

475 else: 

476 msg = _('Share section %s already defined.') % share_name 

477 raise exception.ShareBackendException(msg=msg) 

478 

479 for param, value in self.parameters.items(): 

480 self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', 

481 share_name, param, value]) 

482 

483 return self.get_exports_for_share(server, '\\\\%s\\' + share_name) 

484 

485 def remove_exports(self, server, share_name): 

486 """Remove share definition from samba server.""" 

487 try: 

488 self._ssh_exec( 

489 server, ['sudo', 'net', 'conf', 'delshare', share_name]) 

490 except exception.ProcessExecutionError as e: 

491 LOG.warning("Caught error trying delete share: %(error)s, try" 

492 "ing delete it forcibly.", {'error': e.stderr}) 

493 self._ssh_exec(server, ['sudo', 'smbcontrol', 'all', 'close-share', 

494 share_name]) 

495 

496 def update_access(self, server, share_name, access_rules, add_rules, 

497 delete_rules): 

498 """Update access rules for given share. 

499 

500 Please refer to base class for a more in-depth description. For this 

501 specific implementation, add_rules and delete_rules parameters are not 

502 used. 

503 """ 

504 hosts = [] 

505 

506 self.validate_access_rules( 

507 access_rules, ('ip',), (const.ACCESS_LEVEL_RW,)) 

508 

509 for access in access_rules: 

510 hosts.append(access['access_to']) 

511 self._set_allow_hosts(server, hosts, share_name) 

512 

513 def _get_allow_hosts(self, server, share_name): 

514 (out, _) = self._ssh_exec(server, ['sudo', 'net', 'conf', 'getparm', 

515 share_name, 'hosts allow']) 

516 return out.split() 

517 

518 def _set_allow_hosts(self, server, hosts, share_name): 

519 value = ' '.join(hosts) or ' ' 

520 self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', share_name, 

521 'hosts allow', value]) 

522 

523 def get_share_path_by_export_location(self, server, export_location): 

524 # Get name of group that contains share data on CIFS server 

525 group_name = self._get_share_group_name_from_export_location( 

526 export_location) 

527 

528 # Get parameter 'path' from group that belongs to current share 

529 (out, __) = self._ssh_exec( 

530 server, ['sudo', 'net', 'conf', 'getparm', group_name, 'path']) 

531 

532 # Remove special symbols from response and return path 

533 return out.strip() 

534 

535 def disable_access_for_maintenance(self, server, share_name): 

536 maintenance_file = self._get_maintenance_file_path(share_name) 

537 allowed_hosts = " ".join(self._get_allow_hosts(server, share_name)) 

538 

539 backup_exports = [ 

540 'echo', "'%s'" % allowed_hosts, '|', 'sudo', 'tee', 

541 maintenance_file 

542 ] 

543 self._ssh_exec(server, backup_exports) 

544 self._set_allow_hosts(server, [], share_name) 

545 self._kick_out_users(server, share_name) 

546 

547 def _kick_out_users(self, server, share_name): 

548 """Kick out all users of share""" 

549 (out, _) = self._ssh_exec(server, ['sudo', 'smbstatus', '-S']) 

550 

551 shares = [] 

552 header = True 

553 regexp = r"^(?P<share>[^ ]+)\s+(?P<pid>[0-9]+)\s+(?P<machine>[^ ]+).*" 

554 for line in out.splitlines(): 

555 line = line.strip() 

556 if not header and line: 

557 match = re.match(regexp, line) 

558 if match: 

559 shares.append(match.groupdict()) 

560 else: 

561 raise exception.ShareBackendException( 

562 msg="Failed to obtain smbstatus for %s!" % share_name) 

563 elif line.startswith('----'): 

564 header = False 

565 to_kill = [s['pid'] for s in shares if 

566 share_name == s['share'] or share_name is None] 

567 if to_kill: 

568 self._ssh_exec(server, ['sudo', 'kill', '-15'] + to_kill) 

569 

570 def restore_access_after_maintenance(self, server, share_name): 

571 maintenance_file = self._get_maintenance_file_path(share_name) 

572 (exports, __) = self._ssh_exec(server, ['cat', maintenance_file]) 

573 self._set_allow_hosts(server, exports.split(), share_name) 

574 self._ssh_exec(server, ['sudo', 'rm', '-f', maintenance_file]) 

575 

576 

577class CIFSHelperUserAccess(CIFSHelperIPAccess): 

578 """Manage shares in samba server by net conf tool. 

579 

580 Class provides functionality to operate with CIFS shares. 

581 Samba server should be configured to use registry as configuration 

582 backend to allow dynamically share managements. This class allows 

583 to define access to shares by usernames with either RW or RO access levels. 

584 """ 

585 def __init__(self, *args): 

586 super(CIFSHelperUserAccess, self).__init__(*args) 

587 self.parameters = { 

588 'browseable': 'yes', 

589 'create mask': '0755', 

590 'hosts allow': '0.0.0.0/0', 

591 'read only': 'no', 

592 } 

593 

594 def update_access(self, server, share_name, access_rules, add_rules, 

595 delete_rules): 

596 """Update access rules for given share. 

597 

598 Please refer to base class for a more in-depth description. For this 

599 specific implementation, add_rules and delete_rules parameters are not 

600 used. 

601 """ 

602 all_users_rw = [] 

603 all_users_ro = [] 

604 

605 self.validate_access_rules( 

606 access_rules, ('user',), 

607 (const.ACCESS_LEVEL_RO, const.ACCESS_LEVEL_RW)) 

608 

609 for access in access_rules: 

610 if access['access_level'] == const.ACCESS_LEVEL_RW: 

611 all_users_rw.append(access['access_to']) 

612 else: 

613 all_users_ro.append(access['access_to']) 

614 self._set_valid_users( 

615 server, all_users_rw, share_name, const.ACCESS_LEVEL_RW) 

616 self._set_valid_users( 

617 server, all_users_ro, share_name, const.ACCESS_LEVEL_RO) 

618 

619 def _get_conf_param(self, access_level): 

620 if access_level == const.ACCESS_LEVEL_RW: 

621 return 'valid users' 

622 else: 

623 return 'read list' 

624 

625 def _set_valid_users(self, server, users, share_name, access_level): 

626 value = ' '.join(users) 

627 param = self._get_conf_param(access_level) 

628 self._ssh_exec(server, ['sudo', 'net', 'conf', 'setparm', share_name, 

629 param, value])