Coverage for manila/share/drivers/dell_emc/plugins/powerflex/connection.py: 94%

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

17PowerFlex 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.powerflex import ( 

28 object_manager as manager) 

29 

30"""Version history: 

31 1.0 - Initial version 

32""" 

33 

34VERSION = "1.0" 

35 

36CONF = cfg.CONF 

37 

38LOG = log.getLogger(__name__) 

39 

40POWERFLEX_OPTS = [ 

41 cfg.StrOpt('powerflex_storage_pool', 

42 help='Storage pool used to provision NAS.'), 

43 cfg.StrOpt('powerflex_protection_domain', 

44 help='Protection domain to use.'), 

45 cfg.StrOpt('dell_nas_backend_host', 

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

47 cfg.IntOpt('dell_nas_backend_port', 

48 default=443, 

49 help='Port number to use with the Dell NAS backend.'), 

50 cfg.StrOpt('dell_nas_server', 

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

52 cfg.StrOpt('dell_nas_login', 

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

54 cfg.StrOpt('dell_nas_password', 

55 secret=True, 

56 help='Password for the Dell NAS backend.') 

57 

58] 

59 

60 

61class PowerFlexStorageConnection(driver.StorageConnection): 

62 """Implements PowerFlex specific functionality for Dell Manila driver.""" 

63 

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

65 """Do initialization""" 

66 

67 LOG.debug('Invoking base constructor for Manila \ 

68 Dell PowerFlex SDNAS Driver.') 

69 super(PowerFlexStorageConnection, 

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

71 

72 LOG.debug('Setting up attributes for Manila \ 

73 Dell PowerFlex SDNAS Driver.') 

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

75 kwargs['configuration'].append_config_values(POWERFLEX_OPTS) 

76 

77 self.manager = None 

78 self.server = None 

79 self._username = None 

80 self._password = None 

81 self._server_url = None 

82 self._root_dir = None 

83 self._verify_ssl_cert = None 

84 self._shares = {} 

85 self.verify_certificate = None 

86 self.certificate_path = None 

87 self.export_path = None 

88 

89 self.driver_handles_share_servers = False 

90 

91 self.reserved_percentage = None 

92 self.reserved_snapshot_percentage = None 

93 self.reserved_share_extend_percentage = None 

94 self.max_over_subscription_ratio = None 

95 

96 def connect(self, dell_share_driver, context): 

97 """Connects to Dell PowerFlex SDNAS server.""" 

98 LOG.debug('Reading configuration parameters for Manila \ 

99 Dell PowerFlex SDNAS Driver.') 

100 config = dell_share_driver.configuration 

101 get_config_value = config.safe_get 

102 self.verify_certificate = get_config_value("dell_ssl_cert_verify") 

103 self.rest_ip = get_config_value("dell_nas_backend_host") 

104 self.rest_port = get_config_value("dell_nas_backend_port") 

105 self.nas_server = get_config_value("dell_nas_server") 

106 self.storage_pool = get_config_value("powerflex_storage_pool") 

107 self.protection_domain = get_config_value( 

108 "powerflex_protection_domain") 

109 self.rest_username = get_config_value("dell_nas_login") 

110 self.rest_password = get_config_value("dell_nas_password") 

111 if self.verify_certificate: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true

112 self.certificate_path = get_config_value( 

113 "dell_ssl_certificate_path") 

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

115 self.rest_username, 

116 self.rest_password]): 

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

118 " must be specified.") 

119 raise exception.BadConfigurationException(reason=message) 

120 # validate certificate settings 

121 if self.verify_certificate and not self.certificate_path: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true

122 message = _("Path to REST server's certificate must be specified.") 

123 raise exception.BadConfigurationException(reason=message) 

124 

125 LOG.debug('Initializing Dell PowerFlex SDNAS Layer.') 

126 self.host_url = ("https://%(server_ip)s:%(server_port)s" % 

127 { 

128 "server_ip": self.rest_ip, 

129 "server_port": self.rest_port}) 

130 LOG.info("REST server IP: %(ip)s, port: %(port)s, " 

131 "username: %(user)s. Verify server's certificate: " 

132 "%(verify_cert)s.", 

133 { 

134 "ip": self.rest_ip, 

135 "port": self.rest_port, 

136 "user": self.rest_username, 

137 "verify_cert": self.verify_certificate, 

138 }) 

139 

140 self.manager = manager.StorageObjectManager(self.host_url, 

141 self.rest_username, 

142 self.rest_password, 

143 self.export_path, 

144 self.certificate_path, 

145 self.verify_certificate) 

146 

147 # configuration for share status update 

148 self.reserved_percentage = config.safe_get( 

149 'reserved_share_percentage') 

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

151 self.reserved_percentage = 0 

152 

153 self.reserved_snapshot_percentage = config.safe_get( 

154 'reserved_share_from_snapshot_percentage') 

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

156 self.reserved_snapshot_percentage = self.reserved_percentage 

157 

158 self.reserved_share_extend_percentage = config.safe_get( 

159 'reserved_share_extend_percentage') 

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

161 self.reserved_share_extend_percentage = self.reserved_percentage 

162 

163 self.max_over_subscription_ratio = config.safe_get( 

164 'max_over_subscription_ratio') 

165 

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

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

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

169 location = self._create_nfs_share(share) 

170 

171 return location 

172 

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

174 share_server=None, parent_share=None): 

175 """Is called to create a share from an existing snapshot.""" 

176 raise NotImplementedError() 

177 

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

179 """Is called to allow access to a share.""" 

180 raise NotImplementedError() 

181 

182 def check_for_setup_error(self): 

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

184 

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

186 delete_rules, share_server=None): 

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

188 LOG.debug(f'Updating access to {share["share_proto"]} share.') 

189 return self._update_nfs_access(share, access_rules) 

190 

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

192 """Is called to create snapshot.""" 

193 export_name = snapshot['share_name'] 

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

195 filesystem_id = self.manager.get_fsid_from_export_name(export_name) 

196 LOG.debug(f'Retrieving snapshot ID for filesystem {filesystem_id}') 

197 snapshot_id = self.manager.create_snapshot(snapshot['name'], 

198 filesystem_id) 

199 if snapshot_id: 

200 LOG.info("Snapshot %(id)s successfully created.", 

201 {'id': snapshot['id']}) 

202 

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

204 """Is called to delete snapshot.""" 

205 snapshot_name = snapshot['name'] 

206 filesystem_id = self.manager.get_fsid_from_snapshot_name(snapshot_name) 

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

208 snapshot_deleted = self.manager.delete_filesystem(filesystem_id) 

209 if not snapshot_deleted: 

210 message = ( 

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

212 {'snapshot': snapshot['name']}) 

213 LOG.error(message) 

214 raise exception.ShareBackendException(msg=message) 

215 else: 

216 LOG.info("Snapshot %(id)s successfully deleted.", 

217 {'id': snapshot['id']}) 

218 

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

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

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

222 self._delete_nfs_share(share) 

223 

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

225 """Is called to deny access to a share.""" 

226 raise NotImplementedError() 

227 

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

229 """Is called to ensure a share is exported.""" 

230 

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

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

233 # Converts the size from GiB to Bytes 

234 new_size_in_bytes = new_size * units.Gi 

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

236 filesystem_id = self.manager.get_filesystem_id(share['name']) 

237 self.manager.extend_export(filesystem_id, 

238 new_size_in_bytes) 

239 

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

241 """Is called to set up a share server. 

242 

243 Requires driver_handles_share_servers to be True. 

244 """ 

245 raise NotImplementedError() 

246 

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

248 """Is called to teardown a share server. 

249 

250 Requires driver_handles_share_servers to be True. 

251 """ 

252 raise NotImplementedError() 

253 

254 def _create_nfs_share(self, share): 

255 """Creates an NFS share. 

256 

257 In PowerFlex, an export (share) belongs to a filesystem. 

258 This function creates a filesystem and an export. 

259 """ 

260 LOG.debug(f'Retrieving Storage Pool ID for {self.storage_pool}') 

261 storage_pool_id = self.manager.get_storage_pool_id( 

262 self.protection_domain, 

263 self.storage_pool) 

264 nas_server_id = self.manager.get_nas_server_id(self.nas_server) 

265 LOG.debug(f"Creating filesystem {share['name']}") 

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

267 filesystem_id = self.manager.create_filesystem(storage_pool_id, 

268 self.nas_server, 

269 share['name'], 

270 size_in_bytes) 

271 if not filesystem_id: 

272 message = { 

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

274 ' was not created.') % 

275 {'export': share['name']}} 

276 LOG.error(message) 

277 raise exception.ShareBackendException(msg=message) 

278 

279 LOG.debug(f"Creating export {share['name']}") 

280 export_id = self.manager.create_nfs_export(filesystem_id, 

281 share['name']) 

282 if not export_id: 

283 message = ( 

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

285 ' was not created.') % 

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

287 LOG.error(message) 

288 raise exception.ShareBackendException(msg=message) 

289 file_interfaces = self.manager.get_nas_server_interfaces( 

290 nas_server_id) 

291 export_path = self.manager.get_nfs_export_name(export_id) 

292 locations = self._get_nfs_location(file_interfaces, 

293 export_path) 

294 return locations 

295 

296 def _delete_nfs_share(self, share): 

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

298 filesystem_id = self.manager.get_filesystem_id(share['name']) 

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

300 if filesystem_id is None: 

301 message = ('Attempted to delete NFS export "%s",' 

302 ' but the export does not appear to exist.') 

303 LOG.warning(message, share['name']) 

304 else: 

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

306 share_deleted = self.manager.delete_filesystem(filesystem_id) 

307 if not share_deleted: 

308 message = ( 

309 _('Failed to delete NFS export "%(export)s".') % 

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

311 LOG.error(message) 

312 raise exception.ShareBackendException(msg=message) 

313 

314 def _update_nfs_access(self, share, access_rules): 

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

316 nfs_rw_ips = set() 

317 nfs_ro_ips = set() 

318 access_updates = {} 

319 

320 for rule in access_rules: 

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

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

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

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

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

326 LOG.error(message) 

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

328 

329 else: 

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

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

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

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

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

335 

336 share_id = self.manager.get_nfs_export_id(share['name']) 

337 share_updated = self.manager.set_export_access(share_id, 

338 nfs_rw_ips, 

339 nfs_ro_ips) 

340 if not share_updated: 

341 message = ( 

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

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

344 LOG.error(message) 

345 raise exception.ShareBackendException(msg=message) 

346 return access_updates 

347 

348 def update_share_stats(self, stats_dict): 

349 """Retrieve stats info from share.""" 

350 stats_dict['driver_version'] = VERSION 

351 stats_dict['storage_protocol'] = 'NFS' 

352 stats_dict['create_share_from_snapshot_support'] = False 

353 stats_dict['pools'] = [] 

354 storage_pool_id = self.manager.get_storage_pool_id( 

355 self.protection_domain, 

356 self.storage_pool 

357 ) 

358 total = free = used = provisioned = 0 

359 statistic = self.manager.get_storage_pool_statistic(storage_pool_id) 

360 if statistic: 360 ↛ 365line 360 didn't jump to line 365 because the condition on line 360 was always true

361 total = statistic.get('maxCapacityInKb') // units.Mi 

362 free = statistic.get('netUnusedCapacityInKb') // units.Mi 

363 used = statistic.get('capacityInUseInKb') // units.Mi 

364 provisioned = statistic.get('primaryVacInKb') // units.Mi 

365 pool_stat = { 

366 'pool_name': self.storage_pool, 

367 'thin_provisioning': True, 

368 'total_capacity_gb': total, 

369 'free_capacity_gb': free, 

370 'allocated_capacity_gb': used, 

371 'provisioned_capacity_gb': provisioned, 

372 'qos': False, 

373 'reserved_percentage': self.reserved_percentage, 

374 'reserved_snapshot_percentage': 

375 self.reserved_snapshot_percentage, 

376 'reserved_share_extend_percentage': 

377 self.reserved_share_extend_percentage, 

378 'max_over_subscription_ratio': 

379 self.max_over_subscription_ratio 

380 } 

381 stats_dict['pools'].append(pool_stat) 

382 

383 def _get_nfs_location(self, file_interfaces, export_path): 

384 export_locations = [] 

385 for interface in file_interfaces: 

386 export_locations.append( 

387 {'path': f"{interface}:/{export_path}"}) 

388 return export_locations 

389 

390 def get_default_filter_function(self): 

391 return 'share.size >= 3'