Coverage for manila/share/drivers/dell_emc/plugins/powerstore/connection.py: 96%

269 statements  

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

1# Copyright (c) 2023 Dell Inc. or its subsidiaries. 

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""" 

17PowerStore specific NAS backend plugin. 

18""" 

19from oslo_config import cfg 

20from oslo_log import log 

21from oslo_utils import units 

22 

23from manila.common import constants as const 

24from manila import exception 

25from manila.i18n import _ 

26from manila.share.drivers.dell_emc.plugins import base as driver 

27from manila.share.drivers.dell_emc.plugins.powerstore import client 

28 

29"""Version history: 

30 1.0 - Initial version 

31""" 

32VERSION = "1.0" 

33 

34CONF = cfg.CONF 

35 

36LOG = log.getLogger(__name__) 

37 

38POWERSTORE_OPTS = [ 

39 cfg.StrOpt('dell_nas_backend_host', 

40 help='Dell NAS backend hostname or IP address.'), 

41 cfg.StrOpt('dell_nas_server', 

42 help='Root directory or NAS server which owns the shares.'), 

43 cfg.StrOpt('dell_ad_domain', 

44 help='Domain name of the active directory ' 

45 'joined by the NAS server.'), 

46 cfg.StrOpt('dell_nas_login', 

47 help='User name for the Dell NAS backend.'), 

48 cfg.StrOpt('dell_nas_password', 

49 secret=True, 

50 help='Password for the Dell NAS backend.'), 

51 cfg.BoolOpt('dell_ssl_cert_verify', 

52 default=False, 

53 help='If set to False the https client will not validate the ' 

54 'SSL certificate of the backend endpoint.'), 

55 cfg.StrOpt('dell_ssl_cert_path', 

56 help='Can be used to specify a non default path to a ' 

57 'CA_BUNDLE file or directory with certificates of trusted ' 

58 'CAs, which will be used to validate the backend.') 

59] 

60 

61 

62class PowerStoreStorageConnection(driver.StorageConnection): 

63 """Implements PowerStore specific functionality for Dell Manila driver.""" 

64 

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

66 """Do initialization""" 

67 

68 LOG.debug('Invoking base constructor for Manila' 

69 ' Dell PowerStore Driver.') 

70 super(PowerStoreStorageConnection, 

71 self).__init__(*args, **kwargs) 

72 

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

74 ' Dell PowerStore Driver.') 

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

76 kwargs['configuration'].append_config_values(POWERSTORE_OPTS) 

77 

78 self.client = None 

79 self.verify_certificate = None 

80 self.certificate_path = None 

81 self.ipv6_implemented = True 

82 self.revert_to_snap_support = True 

83 self.shrink_share_support = True 

84 

85 # props from super class 

86 self.driver_handles_share_servers = False 

87 # props for share status update 

88 self.reserved_percentage = None 

89 self.reserved_snapshot_percentage = None 

90 self.reserved_share_extend_percentage = None 

91 self.max_over_subscription_ratio = None 

92 

93 def connect(self, dell_share_driver, context): 

94 """Connects to Dell PowerStore""" 

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

96 ' Dell PowerStore Driver.') 

97 config = dell_share_driver.configuration 

98 get_config_value = config.safe_get 

99 self.rest_ip = get_config_value("dell_nas_backend_host") 

100 self.rest_username = get_config_value("dell_nas_login") 

101 self.rest_password = get_config_value("dell_nas_password") 

102 # validate IP, username and password 

103 if not all([self.rest_ip, 103 ↛ 106line 103 didn't jump to line 106 because the condition on line 103 was never true

104 self.rest_username, 

105 self.rest_password]): 

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

107 " must be specified.") 

108 raise exception.BadConfigurationException(reason=message) 

109 self.nas_server = get_config_value("dell_nas_server") 

110 self.ad_domain = get_config_value("dell_ad_domain") 

111 self.verify_certificate = (get_config_value("dell_ssl_cert_verify") or 

112 False) 

113 if self.verify_certificate: 113 ↛ 117line 113 didn't jump to line 117 because the condition on line 113 was always true

114 self.certificate_path = get_config_value( 

115 "dell_ssl_cert_path") 

116 

117 LOG.debug('Initializing Dell PowerStore REST Client.') 

118 LOG.info("REST server IP: %(ip)s, username: %(user)s. " 

119 "Verify server's certificate: %(verify_cert)s.", 

120 { 

121 "ip": self.rest_ip, 

122 "user": self.rest_username, 

123 "verify_cert": self.verify_certificate, 

124 }) 

125 

126 self.client = client.PowerStoreClient(self.rest_ip, 

127 self.rest_username, 

128 self.rest_password, 

129 self.verify_certificate, 

130 self.certificate_path) 

131 

132 # configuration for share status update 

133 self.reserved_percentage = config.safe_get( 

134 'reserved_share_percentage') 

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

136 self.reserved_percentage = 0 

137 

138 self.reserved_snapshot_percentage = config.safe_get( 

139 'reserved_share_from_snapshot_percentage') 

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

141 self.reserved_snapshot_percentage = self.reserved_percentage 

142 

143 self.reserved_share_extend_percentage = config.safe_get( 

144 'reserved_share_extend_percentage') 

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

146 self.reserved_share_extend_percentage = self.reserved_percentage 

147 

148 self.max_over_subscription_ratio = config.safe_get( 

149 'max_over_subscription_ratio') 

150 

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

152 """Is called to create a share.""" 

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

154 locations = self._create_share(share) 

155 return locations 

156 

157 def _create_share(self, share): 

158 """Creates a NFS or SMB share. 

159 

160 In PowerStore, an export (share) belongs to a filesystem. 

161 This function creates a filesystem and an export. 

162 """ 

163 share_name = share['name'] 

164 size_in_bytes = share['size'] * units.Gi 

165 # create a filesystem 

166 nas_server_id = self.client.get_nas_server_id(self.nas_server) 

167 LOG.debug(f"Creating filesystem {share_name}") 

168 filesystem_id = self.client.create_filesystem(nas_server_id, 

169 share_name, 

170 size_in_bytes) 

171 if not filesystem_id: 

172 message = { 

173 _('The filesystem "%(export)s" was not created.') % 

174 {'export': share_name}} 

175 LOG.error(message) 

176 raise exception.ShareBackendException(msg=message) 

177 # create a share 

178 locations = self._create_share_NFS_CIFS(nas_server_id, filesystem_id, 

179 share_name, 

180 share['share_proto'].upper()) 

181 return locations 

182 

183 def _create_share_NFS_CIFS(self, nas_server_id, filesystem_id, share_name, 

184 protocol): 

185 LOG.debug(f"Get file interfaces of {nas_server_id}") 

186 file_interfaces = self.client.get_nas_server_interfaces( 

187 nas_server_id) 

188 LOG.debug(f"Creating {protocol} export {share_name}") 

189 if protocol == 'NFS': 

190 export_id = self.client.create_nfs_export(filesystem_id, 

191 share_name) 

192 if not export_id: 

193 message = ( 

194 _('The requested NFS export "%(export)s"' 

195 ' was not created.') % 

196 {'export': share_name}) 

197 LOG.error(message) 

198 raise exception.ShareBackendException(msg=message) 

199 locations = self._get_nfs_location(file_interfaces, share_name) 

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

201 export_id = self.client.create_smb_share(filesystem_id, 

202 share_name) 

203 if not export_id: 

204 message = ( 

205 _('The requested SMB share "%(export)s"' 

206 ' was not created.') % 

207 {'export': share_name}) 

208 LOG.error(message) 

209 raise exception.ShareBackendException(msg=message) 

210 locations = self._get_cifs_location(file_interfaces, 

211 share_name) 

212 return locations 

213 

214 def _get_nfs_location(self, file_interfaces, share_name): 

215 export_locations = [] 

216 for interface in file_interfaces: 

217 export_locations.append( 

218 {'path': f"{interface['ip']}:/{share_name}", 

219 'metadata': { 

220 'preferred': interface['preferred'] 

221 } 

222 }) 

223 return export_locations 

224 

225 def _get_cifs_location(self, file_interfaces, share_name): 

226 export_locations = [] 

227 for interface in file_interfaces: 

228 export_locations.append( 

229 {'path': f"\\\\{interface['ip']}\\{share_name}", 

230 'metadata': { 

231 'preferred': interface['preferred'] 

232 } 

233 }) 

234 return export_locations 

235 

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

237 """Is called to delete a share.""" 

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

239 self._delete_share(share) 

240 

241 def _delete_share(self, share): 

242 """Deletes a filesystem and its associated export.""" 

243 LOG.debug(f"Retrieving filesystem ID for filesystem {share['name']}") 

244 filesystem_id = self.client.get_filesystem_id(share['name']) 

245 if not filesystem_id: 

246 LOG.warning( 

247 f'Filesystem with share name {share["name"]} is not found.') 

248 else: 

249 LOG.debug(f"Deleting filesystem ID {filesystem_id}") 

250 share_deleted = self.client.delete_filesystem(filesystem_id) 

251 if not share_deleted: 

252 message = ( 

253 _('Failed to delete share "%(export)s".') % 

254 {'export': share['name']}) 

255 LOG.error(message) 

256 raise exception.ShareBackendException(msg=message) 

257 

258 def extend_share(self, share, new_size, share_server): 

259 """Is called to extend a share.""" 

260 LOG.debug(f"Extending {share['name']} to {new_size}GiB") 

261 self._resize_filesystem(share, new_size) 

262 

263 def shrink_share(self, share, new_size, share_server): 

264 """Is called to shrink a share.""" 

265 LOG.debug(f"Shrinking {share['name']} to {new_size}GiB") 

266 self._resize_filesystem(share, new_size) 

267 

268 def _resize_filesystem(self, share, new_size): 

269 """Is called to resize a filesystem""" 

270 

271 # Converts the size from GiB to Bytes 

272 new_size_in_bytes = new_size * units.Gi 

273 filesystem_id = self.client.get_filesystem_id(share['name']) 

274 is_success, detail = self.client.resize_filesystem(filesystem_id, 

275 new_size_in_bytes) 

276 if not is_success: 

277 message = (_('Failed to resize share "%(export)s".') % 

278 {'export': share['name']}) 

279 LOG.error(message) 

280 if detail: 

281 raise exception.ShareShrinkingPossibleDataLoss( 

282 share_id=share['id']) 

283 raise exception.ShareBackendException(msg=message) 

284 

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

286 """Allow access to the share.""" 

287 raise NotImplementedError() 

288 

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

290 """Deny access to the share.""" 

291 raise NotImplementedError() 

292 

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

294 delete_rules, share_server=None): 

295 """Is called to update share access.""" 

296 protocol = share['share_proto'].upper() 

297 LOG.debug(f'Updating access to {protocol} share.') 

298 if protocol == 'NFS': 

299 return self._update_nfs_access(share, access_rules) 

300 elif protocol == 'CIFS': 300 ↛ exitline 300 didn't return from function 'update_access' because the condition on line 300 was always true

301 return self._update_cifs_access(share, access_rules) 

302 

303 def _update_nfs_access(self, share, access_rules): 

304 """Updates access rules for NFS share type.""" 

305 nfs_rw_ips = set() 

306 nfs_ro_ips = set() 

307 access_updates = {} 

308 

309 for rule in access_rules: 

310 if rule['access_type'].lower() != 'ip': 

311 message = (_("Only IP access type currently supported for " 

312 "NFS. Share provided %(share)s with rule type " 

313 "%(type)s") % {'share': share['display_name'], 

314 'type': rule['access_type']}) 

315 LOG.error(message) 

316 access_updates.update({rule['access_id']: {'state': 'error'}}) 

317 

318 else: 

319 if rule['access_level'] == const.ACCESS_LEVEL_RW: 

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

321 elif rule['access_level'] == const.ACCESS_LEVEL_RO: 321 ↛ 323line 321 didn't jump to line 323 because the condition on line 321 was always true

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

323 access_updates.update({rule['access_id']: {'state': 'active'}}) 

324 

325 share_id = self.client.get_nfs_export_id(share['name']) 

326 share_updated = self.client.set_export_access(share_id, 

327 nfs_rw_ips, 

328 nfs_ro_ips) 

329 if not share_updated: 

330 message = ( 

331 _('Failed to update NFS access rules for "%(export)s".') % 

332 {'export': share['display_name']}) 

333 LOG.error(message) 

334 raise exception.ShareBackendException(msg=message) 

335 return access_updates 

336 

337 def _update_cifs_access(self, share, access_rules): 

338 """Updates access rules for CIFS share type.""" 

339 cifs_rw_users = set() 

340 cifs_ro_users = set() 

341 access_updates = {} 

342 

343 for rule in access_rules: 

344 if rule['access_type'].lower() != 'user': 

345 message = (_("Only user access type currently supported for " 

346 "CIFS. Share provided %(share)s with rule type " 

347 "%(type)s") % {'share': share['display_name'], 

348 'type': rule['access_type']}) 

349 LOG.error(message) 

350 access_updates.update({rule['access_id']: {'state': 'error'}}) 

351 

352 else: 

353 prefix = ( 

354 self.ad_domain or 

355 self.client.get_nas_server_smb_netbios(self.nas_server) 

356 ) 

357 if not prefix: 

358 message = ( 

359 _('Failed to get daomain/netbios name of ' 

360 '"%(nas_server)s".' 

361 ) % {'nas_server': self.nas_server}) 

362 LOG.error(message) 

363 access_updates.update({rule['access_id']: 

364 {'state': 'error'}}) 

365 continue 

366 

367 prefix = prefix + '\\' 

368 if rule['access_level'] == const.ACCESS_LEVEL_RW: 

369 cifs_rw_users.add(prefix + rule['access_to']) 

370 elif rule['access_level'] == const.ACCESS_LEVEL_RO: 370 ↛ 372line 370 didn't jump to line 372 because the condition on line 370 was always true

371 cifs_ro_users.add(prefix + rule['access_to']) 

372 access_updates.update({rule['access_id']: {'state': 'active'}}) 

373 

374 share_id = self.client.get_smb_share_id(share['name']) 

375 share_updated = self.client.set_acl(share_id, 

376 cifs_rw_users, 

377 cifs_ro_users) 

378 if not share_updated: 

379 message = ( 

380 _('Failed to update NFS access rules for "%(export)s".') % 

381 {'export': share['display_name']}) 

382 LOG.error(message) 

383 raise exception.ShareBackendException(msg=message) 

384 return access_updates 

385 

386 def update_share_stats(self, stats_dict): 

387 """Retrieve stats info from share.""" 

388 stats_dict['driver_version'] = VERSION 

389 stats_dict['storage_protocol'] = 'NFS_CIFS' 

390 stats_dict['reserved_percentage'] = self.reserved_percentage 

391 stats_dict['reserved_snapshot_percentage'] = ( 

392 self.reserved_snapshot_percentage) 

393 stats_dict['reserved_share_extend_percentage'] = ( 

394 self.reserved_share_extend_percentage) 

395 stats_dict['max_over_subscription_ratio'] = ( 

396 self.max_over_subscription_ratio) 

397 

398 cluster_id = self.client.get_cluster_id() 

399 total, used = self.client.retreive_cluster_capacity_metrics(cluster_id) 

400 if total and used: 

401 free = total - used 

402 stats_dict['total_capacity_gb'] = total // units.Gi 

403 stats_dict['free_capacity_gb'] = free // units.Gi 

404 

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

406 """Is called to create snapshot.""" 

407 export_name = snapshot['share_name'] 

408 LOG.debug(f'Retrieving filesystem ID for share {export_name}') 

409 filesystem_id = self.client.get_filesystem_id(export_name) 

410 if not filesystem_id: 

411 message = ( 

412 _('Failed to get filesystem id for export "%(export)s".') % 

413 {'export': export_name}) 

414 LOG.error(message) 

415 raise exception.ShareBackendException(msg=message) 

416 snapshot_name = snapshot['name'] 

417 LOG.debug( 

418 f'Creating snapshot {snapshot_name} for filesystem {filesystem_id}' 

419 ) 

420 snapshot_id = self.client.create_snapshot(filesystem_id, 

421 snapshot_name) 

422 if not snapshot_id: 

423 message = ( 

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

425 {'snapshot': snapshot_name}) 

426 LOG.error(message) 

427 raise exception.ShareBackendException(msg=message) 

428 else: 

429 LOG.info("Snapshot %(snapshot)s successfully created.", 

430 {'snapshot': snapshot_name}) 

431 

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

433 """Is called to delete snapshot.""" 

434 snapshot_name = snapshot['name'] 

435 LOG.debug(f'Retrieving filesystem ID for snapshot {snapshot_name}') 

436 filesystem_id = self.client.get_filesystem_id(snapshot_name) 

437 LOG.debug(f'Deleting filesystem ID {filesystem_id}') 

438 snapshot_deleted = self.client.delete_filesystem(filesystem_id) 

439 if not snapshot_deleted: 

440 message = ( 

441 _('Failed to delete snapshot "%(snapshot)s".') % 

442 {'snapshot': snapshot_name}) 

443 LOG.error(message) 

444 raise exception.ShareBackendException(msg=message) 

445 else: 

446 LOG.info("Snapshot %(snapshot)s successfully deleted.", 

447 {'snapshot': snapshot_name}) 

448 

449 def revert_to_snapshot(self, context, snapshot, share_access_rules, 

450 snapshot_access_rules, share_server=None): 

451 """Reverts a share (in place) to the specified snapshot.""" 

452 snapshot_name = snapshot['name'] 

453 snapshot_id = self.client.get_filesystem_id(snapshot_name) 

454 snapshot_restored = self.client.restore_snapshot(snapshot_id) 

455 if not snapshot_restored: 

456 message = ( 

457 _('Failed to restore snapshot "%(snapshot)s".') % 

458 {'snapshot': snapshot_name}) 

459 LOG.error(message) 

460 raise exception.ShareBackendException(msg=message) 

461 else: 

462 LOG.info("Snapshot %(snapshot)s successfully restored.", 

463 {'snapshot': snapshot_name}) 

464 

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

466 share_server=None, parent_share=None): 

467 """Create a share from a snapshot - clone a snapshot.""" 

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

469 locations = self._create_share_from_snapshot(share, snapshot) 

470 

471 if share['size'] != snapshot['size']: 471 ↛ 475line 471 didn't jump to line 475 because the condition on line 471 was always true

472 LOG.debug(f"Resizing {share['name']} to {share['size']}GiB") 

473 self._resize_filesystem(share, share['size']) 

474 

475 return locations 

476 

477 def _create_share_from_snapshot(self, share, snapshot): 

478 LOG.debug(f"Retrieving snapshot id of snapshot {snapshot['name']}") 

479 snapshot_id = self.client.get_filesystem_id(snapshot['name']) 

480 share_name = share['name'] 

481 LOG.debug( 

482 f"Cloning filesystem {share_name} from snapshot {snapshot_id}" 

483 ) 

484 filesystem_id = self.client.clone_snapshot(snapshot_id, 

485 share_name) 

486 if not filesystem_id: 

487 message = { 

488 _('The filesystem "%(export)s" was not created.') % 

489 {'export': share_name}} 

490 LOG.error(message) 

491 raise exception.ShareBackendException(msg=message) 

492 # create a share 

493 nas_server_id = self.client.get_nas_server_id(self.nas_server) 

494 locations = self._create_share_NFS_CIFS(nas_server_id, filesystem_id, 

495 share_name, 

496 share['share_proto'].upper()) 

497 return locations 

498 

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

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

501 

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

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

504 

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

506 """Teardown share server.""" 

507 

508 def check_for_setup_error(self): 

509 """Is called to check for setup error.""" 

510 

511 def get_default_filter_function(self): 

512 return 'share.size >= 3'