Coverage for manila/share/drivers/dell_emc/plugins/powerscale/powerscale.py: 97%

498 statements  

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

1# Copyright 2015 EMC Corporation 

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 

16""" 

17PowerScale specific NAS backend plugin. 

18""" 

19import os 

20 

21from oslo_config import cfg 

22from oslo_log import log 

23from oslo_utils import units 

24 

25from manila.common import constants as const 

26from manila import exception 

27from manila.i18n import _ 

28from manila.share.drivers.dell_emc.plugins import base 

29from manila.share.drivers.dell_emc.plugins.powerscale import powerscale_api 

30 

31"""Version history: 

32 0.1.0 - Initial version 

33 1.0.0 - Fix Http auth issue, SSL verification error and etc 

34 1.0.1 - Add support for update share stats 

35 1.0.2 - Add support for ensure shares 

36 1.0.3 - Add support for thin provisioning 

37 1.0.4 - Rename isilon to powerscale 

38 1.0.5 - Add support for share shrink 

39 1.0.6 - Add support of manage/unmanage share and snapshot 

40 1.0.7 - Add support of mount snapshot 

41 1.0.8 - Add support of mount point name 

42 

43""" 

44VERSION = "1.0.8" 

45 

46CONF = cfg.CONF 

47 

48LOG = log.getLogger(__name__) 

49 

50POWERSCALE_OPTS = [ 

51 cfg.StrOpt('powerscale_dir_permission', 

52 default='0777', 

53 help='Predefined ACL value or POSIX mode ' 

54 'for PowerScale directories.'), 

55 cfg.IntOpt('powerscale_threshold_limit', 

56 default=0, 

57 help='Specifies the threshold limit (in percentage) ' 

58 'for triggering SmartQuotas alerts in PowerScale') 

59] 

60 

61 

62class PowerScaleStorageConnection(base.StorageConnection): 

63 """Implements PowerScale specific functionality for EMC Manila driver.""" 

64 

65 def __init__(self, *args, **kwargs): 

66 super(PowerScaleStorageConnection, self).__init__(*args, **kwargs) 

67 LOG.debug('Setting up attributes for Manila ' 

68 'Dell PowerScale Driver.') 

69 if 'configuration' in kwargs: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true

70 kwargs['configuration'].append_config_values(POWERSCALE_OPTS) 

71 

72 self._server = None 

73 self._port = None 

74 self._username = None 

75 self._password = None 

76 self._server_url = None 

77 self._root_dir = None 

78 self._verify_ssl_cert = None 

79 self._ssl_cert_path = None 

80 self._containers = {} 

81 self._shares = {} 

82 self._snapshots = {} 

83 

84 self._powerscale_api = None 

85 self.driver_handles_share_servers = False 

86 self.ipv6_implemented = True 

87 self.manage_existing_snapshot_support = True 

88 # props for share status update 

89 self.reserved_percentage = None 

90 self.reserved_snapshot_percentage = None 

91 self.reserved_share_extend_percentage = None 

92 self.max_over_subscription_ratio = None 

93 self._threshold_limit = 0 

94 self.shrink_share_support = True 

95 self.manage_existing_support = True 

96 self.mount_snapshot_support = True 

97 self._snapshot_root_dir = '/ifs/.snapshot' 

98 

99 def _get_container_path(self, share): 

100 """Return path to a container.""" 

101 return os.path.join(self._root_dir, share['name']) 

102 

103 def _get_snapshot_path(self, snapshot): 

104 return os.path.join(self._snapshot_root_dir, snapshot['name']) 

105 

106 def create_share(self, context, share, share_server): 

107 """Is called to create share.""" 

108 LOG.debug(f'Creating {share["share_proto"]} share.') 

109 if share['share_proto'] == 'NFS': 

110 location = self._create_nfs_share(share) 

111 elif share['share_proto'] == 'CIFS': 

112 location = self._create_cifs_share(share) 

113 else: 

114 message = (_('Unsupported share protocol: %(proto)s.') % 

115 {'proto': share['share_proto']}) 

116 LOG.error(message) 

117 raise exception.InvalidShare(reason=message) 

118 

119 # apply directory quota based on share size 

120 max_share_size = share['size'] * units.Gi 

121 self._powerscale_api.quota_create( 

122 self._get_container_path(share), 'directory', max_share_size) 

123 

124 return location 

125 

126 def create_share_from_snapshot(self, context, share, snapshot, 

127 share_server): 

128 """Creates a share from the snapshot.""" 

129 LOG.debug(f'Creating {share["share_proto"]} share from snapshot.') 

130 # Create share at new location 

131 location = self.create_share(context, share, share_server) 

132 

133 # Clone snapshot to new location 

134 fq_target_dir = self._get_container_path(share) 

135 self._powerscale_api.clone_snapshot(snapshot['name'], fq_target_dir, 

136 snapshot['provider_location']) 

137 

138 return location 

139 

140 def _create_nfs_share(self, share): 

141 """Is called to create nfs share.""" 

142 LOG.debug(f'Creating NFS share {share["name"]}.') 

143 # Create directory 

144 container_path = self._get_container_path(share) 

145 path = {} 

146 self._create_directory(container_path) 

147 # Create nfs share 

148 share_created = self._powerscale_api.create_nfs_export(container_path) 

149 if not share_created: 

150 message = ( 

151 _('The requested NFS share "%(share)s" was not created.') % 

152 {'share': share['name']}) 

153 LOG.error(message) 

154 raise exception.ShareBackendException(msg=message) 

155 format_path = self._format_nfs_path(container_path) 

156 path[format_path] = True 

157 if share.get('mount_point_name'): 

158 path[format_path] = False 

159 mount_point_name = ( 

160 self._format_nfs_mount_point_name( 

161 share.get('mount_point_name'))) 

162 alias_created = (self._powerscale_api. 

163 create_nfs_export_aliases(mount_point_name, 

164 container_path)) 

165 format_path = self._format_nfs_path(mount_point_name) 

166 path[format_path] = True 

167 if not alias_created: 

168 message = ( 

169 _('Failed to create NFS alias for "%(share)s".') % 

170 {'share': share['name']}) 

171 LOG.error(message) 

172 raise exception.ShareBackendException(msg=message) 

173 return self._get_location(path) 

174 

175 def _create_cifs_share(self, share): 

176 """Is called to create cifs share.""" 

177 LOG.debug(f'Creating CIFS share {share["name"]}.') 

178 # Create directory 

179 container_path = self._get_container_path(share) 

180 self._create_directory(container_path) 

181 # Create smb share 

182 share_name = share['name'] 

183 if share.get('mount_point_name'): 

184 share_name = share.get('mount_point_name') 

185 share_created = self._powerscale_api.create_smb_share( 

186 share_name, container_path) 

187 if not share_created: 

188 message = ( 

189 _('The requested CIFS share "%(share)s" was not created.') % 

190 {'share': share['name']}) 

191 LOG.error(message) 

192 raise exception.ShareBackendException(msg=message) 

193 location = (self. 

194 _get_location({self._format_smb_path(share_name): True})) 

195 return location 

196 

197 def _create_directory(self, path, recursive=False): 

198 """Is called to create a directory.""" 

199 dir_created = self._powerscale_api.create_directory(path, recursive) 

200 if not dir_created: 

201 message = ( 

202 _('Failed to create directory "%(dir)s".') % 

203 {'dir': path}) 

204 LOG.error(message) 

205 raise exception.ShareBackendException(msg=message) 

206 

207 def create_snapshot(self, context, snapshot, share_server): 

208 """Is called to create snapshot.""" 

209 LOG.debug(f'Creating snapshot {snapshot["name"]}.') 

210 snapshot_path = os.path.join(self._root_dir, snapshot['share_name']) 

211 snap_id = self._powerscale_api.create_snapshot( 

212 snapshot['name'], snapshot_path) 

213 result = {} 

214 if snap_id is None: 

215 message = ( 

216 _('Failed to create snapshot "%(snap)s".') % 

217 {'snap': snapshot['name']}) 

218 LOG.error(message) 

219 raise exception.ShareBackendException(msg=message) 

220 result['provider_location'] = snap_id 

221 if snapshot['share']['mount_snapshot_support']: 

222 snap_result = self._create_snap_export_path(snapshot=snapshot, 

223 snap_actual_name=None) 

224 result.update(snap_result) 

225 return result 

226 

227 def delete_share(self, context, share, share_server): 

228 """Is called to remove share.""" 

229 LOG.debug(f'Deleting {share["share_proto"]} share.') 

230 if share['share_proto'] == 'NFS': 

231 self._delete_nfs_share(share) 

232 elif share['share_proto'] == 'CIFS': 

233 self._delete_cifs_share(share) 

234 else: 

235 message = (_('Unsupported share type: %(type)s.') % 

236 {'type': share['share_proto']}) 

237 LOG.warning(message) 

238 return 

239 

240 dir_path = self._get_container_path(share) 

241 # remove quota 

242 self._delete_quota(dir_path) 

243 # remove directory 

244 self._delete_directory(dir_path) 

245 

246 def _delete_quota(self, path): 

247 """Is called to remove quota.""" 

248 quota = self._powerscale_api.quota_get(path, 'directory') 

249 if quota: 

250 LOG.debug(f'Removing quota {quota["id"]}') 

251 deleted = self._powerscale_api.delete_quota(quota['id']) 

252 if not deleted: 

253 message = ( 

254 _('Failed to delete quota "%(quota_id)s" for ' 

255 'directory "%(dir)s".') % 

256 {'quota_id': quota['id'], 'dir': path}) 

257 LOG.error(message) 

258 else: 

259 LOG.warning(f'Quota not found for {path}') 

260 

261 def _delete_directory(self, path): 

262 """Is called to remove directory.""" 

263 path_exist = self._powerscale_api.is_path_existent(path) 

264 if path_exist: 

265 LOG.debug(f'Removing directory {path}') 

266 deleted = self._powerscale_api.delete_path(path, recursive=True) 

267 if not deleted: 

268 message = ( 

269 _('Failed to delete directory "%(dir)s".') % 

270 {'dir': path}) 

271 LOG.error(message) 

272 else: 

273 LOG.warning(f'Directory not found for {path}') 

274 

275 def _delete_nfs_share(self, share): 

276 """Is called to remove nfs share.""" 

277 container_path = self._get_container_path(share) 

278 self._delete_export("NFS", share['name'], container_path) 

279 if share.get('mount_point_name'): 

280 self._delete_nfs_aliases(share['mount_point_name'], 

281 container_path, share['name']) 

282 

283 def _delete_nfs_aliases(self, mount_point_name, 

284 container_path, share_name): 

285 mount_point_name = ( 

286 self._format_nfs_mount_point_name(mount_point_name)) 

287 valid_alias = self._check_valid_aliases(mount_point_name, 

288 container_path) 

289 if valid_alias: 

290 alias_deleted = ( 

291 self._powerscale_api. 

292 delete_nfs_export_aliases(mount_point_name)) 

293 if not alias_deleted: 

294 message = (_('Error deleting NFS alias for ' 

295 'share: %s') % share_name) 

296 LOG.error(message) 

297 raise exception.ShareBackendException(msg=message) 

298 else: 

299 message = (_("NFS alias %(alias_name)s is exist " 

300 "with another share path.") % 

301 {'alias_name': mount_point_name}) 

302 LOG.warning(message) 

303 

304 def _delete_cifs_share(self, share): 

305 """Is called to remove CIFS share.""" 

306 share_name = share['name'] 

307 container_path = self._get_container_path(share) 

308 if share.get('mount_point_name'): 

309 share_name = share.get('mount_point_name') 

310 self._delete_export("CIFS", share_name, container_path) 

311 

312 def delete_snapshot(self, context, snapshot, share_server): 

313 """Is called to remove snapshot.""" 

314 snap_name = snapshot['name'] 

315 proto = snapshot['share']['share_proto'] 

316 snap_path = self._get_snapshot_path(snapshot) 

317 LOG.debug('Deleting snapshot %s', snap_name) 

318 if snapshot.get('provider_location'): 

319 deleted = (self._powerscale_api. 

320 delete_snapshot_by_id(snapshot['provider_location'])) 

321 else: 

322 deleted = self._powerscale_api.delete_snapshot(snapshot['name']) 

323 if not deleted: 

324 message = (_('Failed to delete snapshot "%(snap)s".') % 

325 {'snap': snapshot['name']}) 

326 LOG.error(message) 

327 raise exception.ShareBackendException(msg=message) 

328 self._delete_export(proto, snap_name, snap_path) 

329 

330 def ensure_share(self, context, share, share_server): 

331 """Invoked to ensure that share is exported.""" 

332 raise NotImplementedError() 

333 

334 def extend_share(self, share, new_size, share_server=None): 

335 """Extends a share.""" 

336 LOG.debug('Extending share %(name)s to %(size)sG.', { 

337 'name': share['name'], 'size': new_size 

338 }) 

339 new_quota_size = new_size * units.Gi 

340 self._powerscale_api.quota_set( 

341 self._get_container_path(share), 'directory', new_quota_size) 

342 

343 def manage_existing(self, share, driver_options): 

344 """Import an external NFS/CIFS share into Manila.""" 

345 export_path = ( 

346 share.get('export_location') 

347 or share.get('export_locations', [None])[0] 

348 ) 

349 protocol = share.get('share_proto') 

350 LOG.info( 

351 "Managing existing share with protocol: %s, export path: %s", 

352 protocol, 

353 export_path, 

354 ) 

355 if protocol == 'NFS': 

356 nfs_path = export_path.split(':', 1)[1] 

357 export_id = self._powerscale_api.lookup_nfs_export(nfs_path) 

358 if not export_id: 

359 raise exception.ShareBackendException( 

360 msg=f"NFS export {nfs_path} not found." 

361 ) 

362 backend_quota_path = nfs_path 

363 export_location = export_path 

364 elif protocol == 'CIFS': 364 ↛ 380line 364 didn't jump to line 380 because the condition on line 364 was always true

365 share_name = export_path.split('\\')[-1] 

366 smb_share = self._powerscale_api.lookup_smb_share(share_name) 

367 if not smb_share: 

368 raise exception.ShareBackendException( 

369 msg=f"CIFS share {share_name} not found." 

370 ) 

371 backend_quota_path = smb_share.get('path') 

372 if not backend_quota_path: 

373 raise exception.ShareBackendException( 

374 msg=( 

375 "Unable to resolve OneFS path for CIFS share " 

376 f"{share_name}." 

377 ) 

378 ) 

379 export_location = export_path 

380 share_quota = self._powerscale_api.quota_get( 

381 backend_quota_path, 'directory' 

382 ) 

383 size_bytes = ( 

384 share_quota.get('thresholds', {}).get('hard') 

385 if share_quota else 0 

386 ) 

387 if not size_bytes: 

388 raise exception.ManageInvalidShare( 

389 reason=( 

390 "Managing existing share requires a directory quota hard " 

391 "limit on backend path %s." % backend_quota_path 

392 ) 

393 ) 

394 size_gb = size_bytes // units.Gi 

395 return { 

396 'size': size_gb, 

397 'export_locations': [export_location], 

398 } 

399 

400 def shrink_share(self, share, new_size, share_server=None): 

401 """Shrink a share by lowering its directory hard quota. 

402 

403 Rejects shrink if current logical usage exceeds the requested size. 

404 """ 

405 LOG.debug( 

406 'Shrinking share %(name)s to %(size)sG.', 

407 {'name': share['name'], 'size': new_size} 

408 ) 

409 path = self._get_container_path(share) 

410 quota_json = self._powerscale_api.quota_get(path, 'directory') 

411 used_bytes = 0 

412 if quota_json: 412 ↛ 414line 412 didn't jump to line 414 because the condition on line 412 was always true

413 used_bytes = quota_json.get('usage', {}).get('logical', 0) 

414 new_quota_bytes = new_size * units.Gi 

415 if used_bytes > new_quota_bytes: 

416 message = (_( 

417 'Cannot shrink share "%(name)s" to %(size)sG: ' 

418 'used space (%(used)s bytes) exceeds new size.' 

419 ) % { 

420 'name': share['name'], 

421 'size': new_size, 

422 'used': used_bytes 

423 }) 

424 LOG.error(message) 

425 raise exception.ShareShrinkingPossibleDataLoss( 

426 share_id=share.get('id', share.get('name')), 

427 reason=message, 

428 ) 

429 self._powerscale_api.quota_set(path, 'directory', new_quota_bytes) 

430 

431 def allow_access(self, context, share, access, share_server): 

432 """Allow access to the share.""" 

433 raise NotImplementedError() 

434 

435 def deny_access(self, context, share, access, share_server): 

436 """Deny access to the share.""" 

437 raise NotImplementedError() 

438 

439 def check_for_setup_error(self): 

440 """Check for setup error.""" 

441 

442 def connect(self, emc_share_driver, context): 

443 """Connect to an PowerScale cluster.""" 

444 LOG.debug('Reading configuration parameters for Manila' 

445 ' Dell PowerScale Driver.') 

446 config = emc_share_driver.configuration 

447 self._server = config.safe_get("emc_nas_server") 

448 self._port = config.safe_get("emc_nas_server_port") 

449 self._username = config.safe_get("emc_nas_login") 

450 self._password = config.safe_get("emc_nas_password") 

451 self._root_dir = config.safe_get("emc_nas_root_dir") 

452 self._threshold_limit = config.safe_get("powerscale_threshold_limit") 

453 

454 # validate IP, username and password 

455 if not all([self._server, 

456 self._username, 

457 self._password]): 

458 message = _("REST server IP, username and password" 

459 " must be specified.") 

460 raise exception.BadConfigurationException(reason=message) 

461 

462 self._server_url = f'https://{self._server}:{self._port}' 

463 

464 self._verify_ssl_cert = config.safe_get("emc_ssl_cert_verify") 

465 if self._verify_ssl_cert: 465 ↛ 466line 465 didn't jump to line 466 because the condition on line 465 was never true

466 self._ssl_cert_path = config.safe_get("emc_ssl_cert_path") 

467 self._dir_permission = config.safe_get("powerscale_dir_permission") 

468 self._powerscale_api = powerscale_api.PowerScaleApi( 

469 self._server_url, self._username, self._password, 

470 self._verify_ssl_cert, self._ssl_cert_path, 

471 self._dir_permission, 

472 self._threshold_limit) 

473 

474 if not self._powerscale_api.is_path_existent(self._root_dir): 

475 self._create_directory(self._root_dir, recursive=True) 

476 

477 # configuration for share status update 

478 self.reserved_percentage = config.safe_get( 

479 'reserved_share_percentage') 

480 if self.reserved_percentage is None: 480 ↛ 483line 480 didn't jump to line 483 because the condition on line 480 was always true

481 self.reserved_percentage = 0 

482 

483 self.reserved_snapshot_percentage = config.safe_get( 

484 'reserved_share_from_snapshot_percentage') 

485 if self.reserved_snapshot_percentage is None: 485 ↛ 488line 485 didn't jump to line 488 because the condition on line 485 was always true

486 self.reserved_snapshot_percentage = self.reserved_percentage 

487 

488 self.reserved_share_extend_percentage = config.safe_get( 

489 'reserved_share_extend_percentage') 

490 if self.reserved_share_extend_percentage is None: 490 ↛ 493line 490 didn't jump to line 493 because the condition on line 490 was always true

491 self.reserved_share_extend_percentage = self.reserved_percentage 

492 

493 self.max_over_subscription_ratio = config.safe_get( 

494 'max_over_subscription_ratio') 

495 

496 def update_share_stats(self, stats_dict): 

497 """Retrieve stats info from share.""" 

498 stats_dict['driver_version'] = VERSION 

499 stats_dict['storage_protocol'] = 'NFS_CIFS' 

500 # PowerScale does not support pools. 

501 # To align with manila scheduler 'pool-aware' strategic, 

502 # report with one pool structure. 

503 pool_stat = { 

504 'pool_name': stats_dict['share_backend_name'], 

505 'qos': False, 

506 'reserved_percentage': self.reserved_percentage, 

507 'reserved_snapshot_percentage': 

508 self.reserved_snapshot_percentage, 

509 'reserved_share_extend_percentage': 

510 self.reserved_share_extend_percentage, 

511 'max_over_subscription_ratio': 

512 self.max_over_subscription_ratio, 

513 'thin_provisioning': True, 

514 'mount_snapshot_support': True, 

515 'mount_point_name_support': True, 

516 } 

517 spaces = self._powerscale_api.get_space_stats() 

518 if spaces: 518 ↛ 521line 518 didn't jump to line 521 because the condition on line 518 was always true

519 pool_stat['total_capacity_gb'] = spaces['total'] // units.Gi 

520 pool_stat['free_capacity_gb'] = spaces['free'] // units.Gi 

521 allocated_space = self._powerscale_api.get_allocated_space() 

522 pool_stat['allocated_capacity_gb'] = allocated_space 

523 

524 stats_dict['pools'] = [pool_stat] 

525 

526 def get_network_allocations_number(self): 

527 """Returns number of network allocations for creating VIFs.""" 

528 # TODO(Shaun Edwards) 

529 return 0 

530 

531 def setup_server(self, network_info, metadata=None): 

532 """Set up and configures share server with given network parameters.""" 

533 # TODO(Shaun Edwards): Look into supporting share servers 

534 

535 def teardown_server(self, server_details, security_services=None): 

536 """Teardown share server.""" 

537 # TODO(Shaun Edwards): Look into supporting share servers 

538 

539 def update_access(self, context, share, access_rules, add_rules, 

540 delete_rules, share_server=None): 

541 """Update share access.""" 

542 share_name = share["name"] 

543 LOG.debug(f'Updaing access for share {share_name}.') 

544 state_map = {} 

545 if share['share_proto'] == 'NFS': 

546 path = self._get_container_path(share) 

547 state_map = self._update_access_nfs(share_name, path, access_rules) 

548 if share['share_proto'] == 'CIFS': 

549 state_map = self._update_access_cifs(share_name, access_rules) 

550 return state_map 

551 

552 def _update_access_nfs(self, share_name, path, access_rules): 

553 """Updates access on a NFS share.""" 

554 nfs_rw_ips = set() 

555 nfs_ro_ips = set() 

556 rule_state_map = {} 

557 for rule in access_rules: 

558 rule_state_map[rule['access_id']] = { 

559 'state': 'error' 

560 } 

561 

562 for rule in access_rules: 

563 access_level = const.ACCESS_LEVEL_RO 

564 if rule.get('access_level'): 564 ↛ 566line 564 didn't jump to line 566 because the condition on line 564 was always true

565 access_level = rule.get('access_level') 

566 if access_level == const.ACCESS_LEVEL_RW: 

567 nfs_rw_ips.add(rule['access_to']) 

568 elif access_level == const.ACCESS_LEVEL_RO: 568 ↛ 562line 568 didn't jump to line 562 because the condition on line 568 was always true

569 nfs_ro_ips.add(rule['access_to']) 

570 

571 export_id = self._powerscale_api.lookup_nfs_export(path) 

572 if export_id is None: 

573 # share does not exist on backend (set all rules to error state) 

574 message = _('Failed to update access for NFS share %s: ' 

575 'share not found.') % share_name 

576 LOG.error(message) 

577 return rule_state_map 

578 

579 r = self._powerscale_api.modify_nfs_export_access( 

580 export_id, ro_ips=list(nfs_ro_ips), rw_ips=list(nfs_rw_ips)) 

581 if not r: 

582 return rule_state_map 

583 

584 # if we finish the bulk rule update with no error set rules to active 

585 for rule in access_rules: 

586 rule_state_map[rule['access_id']]['state'] = 'active' 

587 return rule_state_map 

588 

589 def _update_access_cifs(self, share_name, access_rules, read_only=False): 

590 """Update access on a CIFS share.""" 

591 rule_state_map = {} 

592 ip_access_rules = [] 

593 user_access_rules = [] 

594 for rule in access_rules: 

595 if rule['access_type'] == 'ip': 

596 ip_access_rules.append(rule) 

597 elif rule['access_type'] == 'user': 

598 user_access_rules.append(rule) 

599 else: 

600 message = (_("Access type %(type)s is not supported for CIFS." 

601 ) % {'type': rule['access_type']}) 

602 LOG.error(message) 

603 rule_state_map.update({rule['access_id']: {'state': 'error'}}) 

604 ips = self._get_cifs_ip_list(ip_access_rules, rule_state_map, 

605 read_only) 

606 user_permissions = self._get_cifs_user_permissions( 

607 user_access_rules, rule_state_map) 

608 if read_only and len(user_permissions) == 0: 

609 user_permissions = [{ 

610 "permission": "read", 

611 "permission_type": "allow", 

612 "trustee": { 

613 "id": "SID:S-1-1-0", 

614 "name": "Everyone", 

615 "type": "wellknown" 

616 } 

617 }] 

618 share_updated = self._powerscale_api.modify_smb_share_access( 

619 share_name, 

620 host_acl=ips, 

621 permissions=user_permissions) 

622 

623 if not share_updated: 

624 message = ( 

625 _( 

626 'Failed to update access rules for CIFS share ' 

627 '"%(share)s".' 

628 ) % {'share': share_name} 

629 ) 

630 LOG.error(message) 

631 for rule in access_rules: 

632 rule_state_map[rule['access_id']] = { 

633 'state': 'error' 

634 } 

635 

636 return rule_state_map 

637 

638 def _get_cifs_ip_list(self, access_rules, rule_state_map, read_only): 

639 """Get CIFS ip list.""" 

640 cifs_ips = [] 

641 for rule in access_rules: 

642 if not read_only and rule['access_level'] != const.ACCESS_LEVEL_RW: 

643 message = ('Only RW access level is supported ' 

644 'for CIFS IP access.') 

645 LOG.error(message) 

646 rule_state_map.update({rule['access_id']: {'state': 'error'}}) 

647 continue 

648 cifs_ips.append('allow:' + rule['access_to']) 

649 rule_state_map.update({rule['access_id']: {'state': 'active'}}) 

650 if len(cifs_ips) > 0: 

651 cifs_ips.append('deny:ALL') 

652 return cifs_ips 

653 

654 def _get_cifs_user_permissions(self, access_rules, rule_state_map): 

655 """Get CIFS user permissions.""" 

656 cifs_user_permissions = [] 

657 for rule in access_rules: 

658 access_level = const.ACCESS_LEVEL_RO 

659 smb_permission = powerscale_api.SmbPermission.ro 

660 if rule.get('access_level'): 660 ↛ 662line 660 didn't jump to line 662 because the condition on line 660 was always true

661 access_level = rule.get('access_level') 

662 if access_level == const.ACCESS_LEVEL_RW: 

663 smb_permission = powerscale_api.SmbPermission.rw 

664 elif access_level == const.ACCESS_LEVEL_RO: 

665 smb_permission = powerscale_api.SmbPermission.ro 

666 user_sid = self._powerscale_api.get_user_sid(rule['access_to']) 

667 if user_sid: 

668 cifs_user_permissions.append({ 

669 'permission': smb_permission.value, 

670 'permission_type': 'allow', 

671 'trustee': user_sid 

672 }) 

673 rule_state_map.update({rule['access_id']: {'state': 'active'}}) 

674 else: 

675 message = _('Failed to get user sid by %(user)s.' % 

676 {'user': rule['access_to']}) 

677 LOG.error(message) 

678 rule_state_map.update({rule['access_id']: {'state': 'error'}}) 

679 return cifs_user_permissions 

680 

681 def get_backend_info(self, context): 

682 """Get driver and array configuration parameters. 

683 

684 :returns: A dictionary containing driver-specific info. 

685 """ 

686 LOG.debug("Retrieving PowerScale backend info.") 

687 cluster_version = self._powerscale_api.get_cluster_version() 

688 return {'driver_version': VERSION, 

689 'cluster_version': cluster_version, 

690 'rest_server': self._server, 

691 'rest_port': self._port} 

692 

693 def ensure_shares(self, context, shares): 

694 """Invoked to ensure that shares are exported. 

695 

696 :shares: A list of all shares for updates. 

697 :returns: None or a dictionary of updates in the format. 

698 """ 

699 LOG.debug("Ensuring PowerScale shares.") 

700 updates = {} 

701 for share in shares: 

702 if share['share_proto'] == 'NFS': 

703 container_path = self._get_container_path(share) 

704 share_id = self._powerscale_api.lookup_nfs_export( 

705 container_path) 

706 if share_id: 

707 location = [self._format_nfs_path(container_path)] 

708 if share.get('mount_point_name'): 

709 location.append(self._format_nfs_path( 

710 self._format_nfs_mount_point_name( 

711 share.get('mount_point_name')) 

712 )) 

713 updates[share['id']] = { 

714 'export_locations': location, 

715 'status': 'available', 

716 'reapply_access_rules': True, 

717 } 

718 else: 

719 LOG.warning(f'NFS Share {share["name"]} is not found.') 

720 elif share['share_proto'] == 'CIFS': 720 ↛ 736line 720 didn't jump to line 736 because the condition on line 720 was always true

721 share_name = share['name'] 

722 if share.get('mount_point_name'): 

723 share_name = share.get('mount_point_name') 

724 smb_share = self._powerscale_api.lookup_smb_share( 

725 share_name) 

726 if smb_share: 

727 location = self._format_smb_path(share_name) 

728 updates[share['id']] = { 

729 'export_locations': [location], 

730 'status': 'available', 

731 'reapply_access_rules': True, 

732 } 

733 else: 

734 LOG.warning(f'CIFS Share {share["name"]} is not found.') 

735 

736 if share['id'] not in updates: 

737 updates[share['id']] = { 

738 'export_locations': [], 

739 'status': 'error', 

740 'reapply_access_rules': False, 

741 } 

742 return updates 

743 

744 def _format_smb_path(self, share_name): 

745 return '\\\\{0}\\{1}'.format(self._server, share_name) 

746 

747 def _format_nfs_path(self, container_path): 

748 return '{0}:{1}'.format(self._server, container_path) 

749 

750 def _get_location(self, paths): 

751 return [ 

752 { 

753 'path': path, 

754 'is_admin_only': False, 

755 'metadata': {'preferred': bool(is_preferred)} 

756 } 

757 for path, is_preferred in paths.items() 

758 ] 

759 

760 def _format_nfs_mount_point_name(self, mount_point_name): 

761 return '/{0}'.format(mount_point_name) 

762 

763 def _check_valid_aliases(self, mount_point_name, 

764 container_path): 

765 alias_details = (self. 

766 _powerscale_api. 

767 get_nfs_export_aliases(mount_point_name)) 

768 return alias_details['path'] == container_path 

769 

770 def manage_existing_snapshot(self, snapshot, driver_options): 

771 """Brings an existing snapshot under Manila management.""" 

772 provider_location = snapshot.get('provider_location') 

773 snap = self._powerscale_api.get_snapshot_id(provider_location) 

774 if not snap: 

775 message = ("Could not find a snapshot in the backend with " 

776 "ID: %s, please make sure " 

777 "the snapshot exists in the backend." 

778 % provider_location) 

779 LOG.error(message) 

780 raise exception.ManageInvalidShareSnapshot(reason=message) 

781 elif (snap['path'] != 

782 self._get_container_path(snapshot['share'])): 

783 message = ("Snapshot does not belong to the given share %s." 

784 % snapshot['share']['name']) 

785 LOG.error(message) 

786 raise exception.ManageInvalidShareSnapshot(reason=message) 

787 try: 

788 snapshot_size = int(driver_options.get("size", 0)) 

789 except (ValueError, TypeError): 

790 msg = _("The size in driver options to manage snapshot " 

791 "%(snap_id)s should be an integer, in format " 

792 "driver-options size=<SIZE>. Value passed: " 

793 "%(size)s.") % {'snap_id': snapshot['id'], 

794 'size': driver_options.get("size")} 

795 LOG.error(msg) 

796 raise exception.ManageInvalidShareSnapshot(reason=msg) 

797 

798 if not snapshot_size: 

799 msg = _("Snapshot %(snap_id)s has no specified size. " 

800 "Use default value to share size, " 

801 "set size in driver options if you " 

802 "want.") % {'snap_id': snapshot['id']} 

803 LOG.info(msg) 

804 snapshot_size = snapshot['share']['size'] 

805 LOG.info("Snapshot %(provider_location)s in " 

806 "PowerScale will be managed " 

807 "with ID %(snapshot_id)s.", 

808 {'provider_location': snapshot.get('provider_location'), 

809 'snapshot_id': snapshot['id']}) 

810 manage_snap_result = {"size": snapshot_size, 

811 "provider_location": provider_location} 

812 if snapshot['share']['mount_snapshot_support']: 

813 snap_result = self._create_snap_export_path( 

814 snapshot=snapshot, 

815 snap_actual_name=snap['name'] 

816 ) 

817 manage_snap_result.update(snap_result) 

818 return manage_snap_result 

819 

820 def _create_snap_export_path(self, snapshot, snap_actual_name=None): 

821 share_proto = snapshot['share']['share_proto'] 

822 snap_export_path = self._get_snapshot_path(snapshot) 

823 snap_name = snapshot['name'] 

824 if snap_actual_name: 

825 snap_name = snap_actual_name 

826 snap_export_path = os.path.join(self._snapshot_root_dir, 

827 snap_name) 

828 created = True 

829 export_path = None 

830 if share_proto == "NFS": 

831 share_export = (self._powerscale_api. 

832 lookup_nfs_export(snap_export_path)) 

833 if share_export is None: 

834 created = (self. 

835 _powerscale_api. 

836 create_snapshot_nfs_export(snap_export_path)) 

837 export_path = self._format_nfs_path(snap_export_path) 

838 elif share_proto == "CIFS": 838 ↛ 846line 838 didn't jump to line 846 because the condition on line 838 was always true

839 smb_export = (self._powerscale_api. 

840 lookup_smb_share(snap_name)) 

841 if smb_export is None: 841 ↛ 845line 841 didn't jump to line 845 because the condition on line 841 was always true

842 created = (self._powerscale_api. 

843 create_snapshot_smb_export(snap_name, 

844 snap_export_path)) 

845 export_path = self._format_smb_path(snap_name) 

846 if not created: 

847 msg = _('Failed to create snapshot export path ' 

848 '"%(snap)s".') % {'snap': snap_name} 

849 LOG.error(msg) 

850 raise exception.ShareBackendException(msg=msg) 

851 return {'export_locations': self._get_location({export_path: True})} 

852 

853 def _delete_export(self, proto, name, path=None): 

854 if proto == "NFS": 

855 share_id = self._powerscale_api.lookup_nfs_export(path) 

856 if share_id is None: 

857 LOG.warning('Attempted to delete NFS export "%s", ' 

858 'but it does not exist.', name) 

859 return 

860 if not self._powerscale_api.delete_nfs_share(share_id): 

861 msg = _('Error deleting NFS export: %s') % name 

862 LOG.error(msg) 

863 raise exception.ShareBackendException(msg=msg) 

864 elif proto == "CIFS": 864 ↛ exitline 864 didn't return from function '_delete_export' because the condition on line 864 was always true

865 smb_share = self._powerscale_api.lookup_smb_share(name) 

866 if smb_share is None: 

867 LOG.warning('Attempted to delete CIFS export "%s", ' 

868 'but it does not exist.', name) 

869 return 

870 elif smb_share['path'] == path: 

871 share_deleted = self._powerscale_api.delete_smb_share(name) 

872 if not share_deleted: 

873 message = (_('Error deleting CIFS share: %s') % 

874 name) 

875 LOG.error(message) 

876 raise exception.ShareBackendException(msg=message) 

877 else: 

878 message = _('CIFS share "%s": the delete operation ' 

879 'failed because the share path ' 

880 'does not match the expected path.') % name 

881 LOG.warning(message) 

882 

883 def snapshot_update_access(self, context, snapshot, 

884 access_rules, add_rules=None, 

885 delete_rules=None, share_server=None): 

886 """Update snapshot access.""" 

887 snapshot_name = snapshot["name"] 

888 LOG.debug(f'Updating access for snapshot {snapshot_name}.') 

889 share_proto = snapshot['share']['share_proto'] 

890 state_map = {} 

891 if share_proto == 'NFS': 

892 path = self._get_snapshot_path(snapshot) 

893 state_map = self._update_access_nfs(snapshot_name, 

894 path, access_rules) 

895 if share_proto == 'CIFS': 

896 state_map = self._update_access_cifs(snapshot_name, 

897 access_rules, 

898 read_only=True) 

899 return state_map