Coverage for manila/share/drivers/purestorage/flashblade.py: 66%

239 statements  

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

1# Copyright 2021 Pure Storage 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""" 

16Pure Storage FlashBlade Share Driver 

17""" 

18 

19import functools 

20import platform 

21 

22from oslo_config import cfg 

23from oslo_log import log as logging 

24from oslo_utils import units 

25 

26from manila import exception 

27from manila.i18n import _ 

28from manila.share import driver 

29 

30try: 

31 import purity_fb 

32except ImportError: 

33 purity_fb = None 

34 

35LOG = logging.getLogger(__name__) 

36 

37flashblade_connection_opts = [ 

38 cfg.HostAddressOpt( 

39 "flashblade_mgmt_vip", 

40 help="The name (or IP address) for the Pure Storage " 

41 "FlashBlade storage system management VIP.", 

42 ), 

43 cfg.ListOpt( 

44 "flashblade_data_vip", 

45 help="The names (or IP address) for the Pure Storage " 

46 "FlashBlade storage system data VIPs. " 

47 "The first listed name or IP address will be considered " 

48 "to be the preferred IP address, although is not " 

49 "enforced.", 

50 ), 

51] 

52 

53flashblade_auth_opts = [ 

54 cfg.StrOpt( 

55 "flashblade_api", 

56 help=("API token for an administrative user account"), 

57 secret=True, 

58 ), 

59] 

60 

61flashblade_extra_opts = [ 

62 cfg.BoolOpt( 

63 "flashblade_eradicate", 

64 default=True, 

65 help="When enabled, all FlashBlade file systems and snapshots " 

66 "will be eradicated at the time of deletion in Manila. " 

67 "Data will NOT be recoverable after a delete with this " 

68 "set to True! When disabled, file systems and snapshots " 

69 "will go into pending eradication state and can be " 

70 "recovered.)", 

71 ), 

72] 

73 

74CONF = cfg.CONF 

75CONF.register_opts(flashblade_connection_opts) 

76CONF.register_opts(flashblade_auth_opts) 

77CONF.register_opts(flashblade_extra_opts) 

78 

79 

80def purity_fb_to_manila_exceptions(func): 

81 @functools.wraps(func) 

82 def wrapper(*args, **kwargs): 

83 try: 

84 return func(*args, **kwargs) 

85 except purity_fb.rest.ApiException as ex: 

86 msg = _("Caught exception from purity_fb: %s") % ex 

87 LOG.exception(msg) 

88 raise exception.ShareBackendException(msg=msg) 

89 

90 return wrapper 

91 

92 

93class FlashBladeShareDriver(driver.ShareDriver): 

94 """Version hisotry: 

95 

96 1.0.0 - Initial version 

97 2.0.0 - Xena release 

98 3.0.0 - Yoga release 

99 4.0.0 - Zed release 

100 5.0.0 - Antelope release 

101 6.0.0 - Bobcat release 

102 7.0.0 - 2024.1 (Caracal) release 

103 8.0.0 - 2025.1 (Epoxy) release 

104 9.0.0 - 2025.2 (Flamingo) release 

105 

106 """ 

107 

108 VERSION = "9.0" # driver version 

109 USER_AGENT_BASE = "OpenStack Manila" 

110 

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

112 super(FlashBladeShareDriver, self).__init__(False, *args, **kwargs) 

113 self.configuration.append_config_values(flashblade_connection_opts) 

114 self.configuration.append_config_values(flashblade_auth_opts) 

115 self.configuration.append_config_values(flashblade_extra_opts) 

116 self._user_agent = "%(base)s %(class)s/%(version)s (%(platform)s)" % { 

117 "base": self.USER_AGENT_BASE, 

118 "class": self.__class__.__name__, 

119 "version": self.VERSION, 

120 "platform": platform.platform(), 

121 } 

122 

123 def do_setup(self, context): 

124 """Driver initialization""" 

125 if purity_fb is None: 

126 msg = _( 

127 "Missing 'purity_fb' python module, ensure the library" 

128 " is installed and available." 

129 ) 

130 raise exception.ManilaException(message=msg) 

131 

132 self.api = self._safe_get_from_config_or_fail("flashblade_api") 

133 self.management_address = self._safe_get_from_config_or_fail( 

134 "flashblade_mgmt_vip" 

135 ) 

136 self.data_address = self._safe_get_from_config_or_fail( 

137 "flashblade_data_vip" 

138 ) 

139 self._sys = purity_fb.PurityFb(self.management_address) 

140 self._sys.disable_verify_ssl() 

141 try: 

142 self._sys.login(self.api) 

143 self._sys._api_client.user_agent = self._user_agent 

144 except purity_fb.rest.ApiException as ex: 

145 msg = _("Exception when logging into the array: %s\n") % ex 

146 LOG.exception(msg) 

147 raise exception.ManilaException(message=msg) 

148 

149 backend_name = self.configuration.safe_get("share_backend_name") 

150 self._backend_name = backend_name or self.__class__.__name__ 

151 

152 LOG.debug("setup complete") 

153 

154 def _update_share_stats(self, data=None): 

155 """Retrieve stats info from share group.""" 

156 ( 

157 free_capacity_bytes, 

158 physical_capacity_bytes, 

159 provisioned_cap_bytes, 

160 data_reduction, 

161 ) = self._get_available_capacity() 

162 

163 reserved_share_percentage = self.configuration.safe_get( 

164 "reserved_share_percentage" 

165 ) 

166 if reserved_share_percentage is None: 

167 reserved_share_percentage = 0 

168 

169 reserved_share_from_snapshot_percentage = self.configuration.safe_get( 

170 "reserved_share_from_snapshot_percentage" 

171 ) 

172 if reserved_share_from_snapshot_percentage is None: 

173 reserved_share_from_snapshot_percentage = reserved_share_percentage 

174 

175 reserved_share_extend_percentage = self.configuration.safe_get( 

176 "reserved_share_extend_percentage" 

177 ) 

178 if reserved_share_extend_percentage is None: 

179 reserved_share_extend_percentage = reserved_share_percentage 

180 

181 data = dict( 

182 share_backend_name=self._backend_name, 

183 vendor_name="PURE STORAGE", 

184 driver_version=self.VERSION, 

185 storage_protocol="NFS", 

186 data_reduction=data_reduction, 

187 reserved_percentage=reserved_share_percentage, 

188 reserved_snapshot_percentage=( 

189 reserved_share_from_snapshot_percentage), 

190 reserved_share_extend_percentage=( 

191 reserved_share_extend_percentage), 

192 total_capacity_gb=float(physical_capacity_bytes) / units.Gi, 

193 free_capacity_gb=float(free_capacity_bytes) / units.Gi, 

194 provisioned_capacity_gb=float(provisioned_cap_bytes) / units.Gi, 

195 snapshot_support=True, 

196 create_share_from_snapshot_support=False, 

197 mount_snapshot_support=False, 

198 revert_to_snapshot_support=True, 

199 thin_provisioning=True, 

200 ) 

201 

202 super(FlashBladeShareDriver, self)._update_share_stats(data) 

203 

204 def _get_available_capacity(self): 

205 try: 

206 space = self._sys.arrays.list_arrays_space() 

207 except purity_fb.rest.ApiException: 

208 message = "Connection failure. Retrying login..." 

209 LOG.warning(message) 

210 try: 

211 self._sys.login(self.api) 

212 self._sys._api_client.user_agent = self._user_agent 

213 except purity_fb.rest.ApiException as ex: 

214 msg = _("Exception when logging into the array: %s\n") % ex 

215 LOG.exception(msg) 

216 raise exception.ManilaException(message=msg) 

217 space = self._sys.arrays.list_arrays_space() 

218 array_space = space.items[0] 

219 data_reduction = array_space.space.data_reduction 

220 physical_capacity_bytes = array_space.capacity 

221 used_capacity_bytes = array_space.space.total_physical 

222 free_capacity_bytes = physical_capacity_bytes - used_capacity_bytes 

223 provisioned_capacity_bytes = array_space.space.unique 

224 return ( 

225 free_capacity_bytes, 

226 physical_capacity_bytes, 

227 provisioned_capacity_bytes, 

228 data_reduction, 

229 ) 

230 

231 def _safe_get_from_config_or_fail(self, config_parameter): 

232 config_value = self.configuration.safe_get(config_parameter) 

233 if not config_value: 

234 reason = _( 

235 "%(config_parameter)s configuration parameter " 

236 "must be specified" 

237 ) % {"config_parameter": config_parameter} 

238 LOG.exception(reason) 

239 raise exception.BadConfigurationException(reason=reason) 

240 return config_value 

241 

242 def _make_source_name(self, snapshot): 

243 base_name = CONF.share_name_template + "-manila" 

244 return base_name % snapshot["share_id"] 

245 

246 def _make_share_name(self, manila_share): 

247 base_name = CONF.share_name_template + "-manila" 

248 return base_name % manila_share["id"] 

249 

250 def _get_full_nfs_export_path(self, export_path, location): 

251 return "{subnet_ip}:/{export_path}".format( 

252 subnet_ip=location, export_path=export_path 

253 ) 

254 

255 def _get_flashblade_filesystem_by_name(self, name): 

256 filesys = [] 

257 filesys.append(name) 

258 try: 

259 res = self._sys.file_systems.list_file_systems(names=filesys) 

260 except purity_fb.rest.ApiException as ex: 

261 msg = _("Share not found on FlashBlade: %s\n") % ex 

262 LOG.exception(msg) 

263 raise exception.ManilaException(message=msg) 

264 message = "Filesystem %(share_name)s exists. Continuing..." 

265 LOG.debug(message, {"share_name": res.items[0].name}) 

266 

267 def _get_flashblade_snapshot_by_name(self, name): 

268 try: 

269 self._sys.file_system_snapshots.list_file_system_snapshots( 

270 filter=name 

271 ) 

272 except purity_fb.rest.ApiException as ex: 

273 msg = _("Snapshot not found on FlashBlade: %s\n") % ex 

274 LOG.exception(msg) 

275 raise exception.ManilaException(message=msg) 

276 

277 @purity_fb_to_manila_exceptions 

278 def _resize_share(self, share, new_size): 

279 dataset_name = self._make_share_name(share) 

280 self._get_flashblade_filesystem_by_name(dataset_name) 

281 consumed_size = ( 

282 self._sys.file_systems.list_file_systems(names=[dataset_name]) 

283 .items[0] 

284 .space.virtual 

285 ) 

286 attr = {} 

287 if consumed_size >= new_size * units.Gi: 

288 raise exception.ShareShrinkingPossibleDataLoss( 

289 share_id=share["id"] 

290 ) 

291 attr["provisioned"] = new_size * units.Gi 

292 n_attr = purity_fb.FileSystem(**attr) 

293 LOG.debug("Resizing filesystem...") 

294 self._sys.file_systems.update_file_systems( 

295 name=dataset_name, attributes=n_attr 

296 ) 

297 

298 def _update_nfs_access(self, share, access_rules): 

299 dataset_name = self._make_share_name(share) 

300 self._get_flashblade_filesystem_by_name(dataset_name) 

301 nfs_rules = "" 

302 rule_state = {} 

303 for access in access_rules: 

304 if access["access_type"] == "ip": 

305 line = ( 

306 access["access_to"] 

307 + "(" 

308 + access["access_level"] 

309 + ",no_root_squash) " 

310 ) 

311 rule_state[access["access_id"]] = {"state": "active"} 

312 nfs_rules += line 

313 else: 

314 message = _( 

315 'Only "ip" access type is allowed for NFS protocol.' 

316 ) 

317 LOG.error(message) 

318 rule_state[access["access_id"]] = {"state": "error"} 

319 try: 

320 self._sys.file_systems.update_file_systems( 

321 name=dataset_name, 

322 attributes=purity_fb.FileSystem( 

323 nfs=purity_fb.NfsRule(rules=nfs_rules) 

324 ), 

325 ) 

326 message = "Set nfs rules %(nfs_rules)s for %(share_name)s" 

327 LOG.debug( 

328 message, {"nfs_rules": nfs_rules, "share_name": dataset_name} 

329 ) 

330 except purity_fb.rest.ApiException as ex: 

331 msg = _("Failed to set NFS access rules: %s\n") % ex 

332 LOG.exception(msg) 

333 raise exception.ManilaException(message=msg) 

334 return rule_state 

335 

336 @purity_fb_to_manila_exceptions 

337 def create_share(self, context, share, share_server=None): 

338 """Create a share and export it based on protocol used.""" 

339 size = share["size"] * units.Gi 

340 share_name = self._make_share_name(share) 

341 

342 if share["share_proto"] == "NFS": 

343 flashblade_fs = purity_fb.FileSystem( 

344 name=share_name, 

345 provisioned=size, 

346 hard_limit_enabled=True, 

347 fast_remove_directory_enabled=True, 

348 snapshot_directory_enabled=True, 

349 nfs=purity_fb.NfsRule( 

350 v3_enabled=True, rules="", v4_1_enabled=True 

351 ), 

352 ) 

353 self._sys.file_systems.create_file_systems(flashblade_fs) 

354 locations = [] 

355 preferred = True 

356 for address in self.data_address: 

357 export_location = { 

358 "path": self._get_full_nfs_export_path( 

359 share_name, 

360 address, 

361 ), 

362 "is_admin_only": False, 

363 "metadata": { 

364 "preferred": preferred, 

365 }, 

366 } 

367 LOG.debug("pref %(pref)s", {"pref": preferred}) 

368 preferred = False 

369 locations.append(export_location) 

370 else: 

371 message = _("Unsupported share protocol: %(proto)s.") % { 

372 "proto": share["share_proto"] 

373 } 

374 LOG.exception(message) 

375 raise exception.InvalidShare(reason=message) 

376 LOG.info("FlashBlade created share %(name)s", {"name": share_name}) 

377 

378 return locations 

379 

380 def create_snapshot(self, context, snapshot, share_server=None): 

381 """Called to create a snapshot""" 

382 source = [] 

383 flashblade_filesystem = self._make_source_name(snapshot) 

384 source.append(flashblade_filesystem) 

385 try: 

386 self._sys.file_system_snapshots.create_file_system_snapshots( 

387 sources=source, suffix=purity_fb.SnapshotSuffix(snapshot["id"]) 

388 ) 

389 except purity_fb.rest.ApiException as ex: 

390 msg = ( 

391 _("Snapshot failed. Share not found on FlashBlade: %s\n") % ex 

392 ) 

393 LOG.exception(msg) 

394 raise exception.ManilaException(message=msg) 

395 

396 def delete_share(self, context, share, share_server=None): 

397 """Called to delete a share""" 

398 dataset_name = self._make_share_name(share) 

399 try: 

400 self._get_flashblade_filesystem_by_name(dataset_name) 

401 except purity_fb.rest.ApiException: 

402 message = ( 

403 "share %(dataset_name)s not found on FlashBlade, skip " 

404 "delete" 

405 ) 

406 LOG.warning(message, {"dataset_name": dataset_name}) 

407 return 

408 self._sys.file_systems.update_file_systems( 

409 name=dataset_name, 

410 attributes=purity_fb.FileSystem( 

411 nfs=purity_fb.NfsRule(v3_enabled=False, v4_1_enabled=False), 

412 smb=purity_fb.ProtocolRule(enabled=False), 

413 destroyed=True, 

414 ), 

415 ) 

416 if self.configuration.flashblade_eradicate: 

417 self._sys.file_systems.delete_file_systems(name=dataset_name) 

418 LOG.info( 

419 "FlashBlade eradicated share %(name)s", {"name": dataset_name} 

420 ) 

421 

422 @purity_fb_to_manila_exceptions 

423 def delete_snapshot(self, context, snapshot, share_server=None): 

424 """Called to delete a snapshot""" 

425 dataset_name = self._make_source_name(snapshot) 

426 filt = "source_display_name='{0}' and suffix='{1}'".format( 

427 dataset_name, snapshot["id"] 

428 ) 

429 name = "{0}.{1}".format(dataset_name, snapshot["id"]) 

430 LOG.debug("FlashBlade filter %(name)s", {"name": filt}) 

431 try: 

432 self._get_flashblade_snapshot_by_name(filt) 

433 except exception.ShareResourceNotFound: 

434 message = ( 

435 "snapshot %(snapshot)s not found on FlashBlade, skip delete" 

436 ) 

437 LOG.warning( 

438 message, {"snapshot": dataset_name + "." + snapshot["id"]} 

439 ) 

440 return 

441 self._sys.file_system_snapshots.update_file_system_snapshots( 

442 name=name, attributes=purity_fb.FileSystemSnapshot(destroyed=True) 

443 ) 

444 LOG.debug( 

445 "Snapshot %(name)s deleted successfully", 

446 {"name": dataset_name + "." + snapshot["id"]}, 

447 ) 

448 if self.configuration.flashblade_eradicate: 

449 self._sys.file_system_snapshots.delete_file_system_snapshots( 

450 name=name 

451 ) 

452 LOG.debug( 

453 "Snapshot %(name)s eradicated successfully", 

454 {"name": dataset_name + "." + snapshot["id"]}, 

455 ) 

456 

457 def ensure_share(self, context, share, share_server=None): 

458 """Dummy - called to ensure share is exported. 

459 

460 All shares created on a FlashBlade are guaranteed to 

461 be exported so this check is redundant 

462 """ 

463 

464 def update_access( 

465 self, 

466 context, 

467 share, 

468 access_rules, 

469 add_rules, 

470 delete_rules, 

471 update_rules, 

472 share_server=None, 

473 ): 

474 """Update access of share""" 

475 # We will use the access_rules list to bulk update access 

476 state_map = self._update_nfs_access(share, access_rules) 

477 return state_map 

478 

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

480 """uses resize_share to extend a share""" 

481 self._resize_share(share, new_size) 

482 

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

484 """uses resize_share to shrink a share""" 

485 self._resize_share(share, new_size) 

486 

487 @purity_fb_to_manila_exceptions 

488 def revert_to_snapshot( 

489 self, 

490 context, 

491 snapshot, 

492 share_access_rules, 

493 snapshot_access_rules, 

494 share_server=None, 

495 ): 

496 dataset_name = self._make_source_name(snapshot) 

497 filt = "source_display_name='{0}' and suffix='{1}'".format( 

498 dataset_name, snapshot["id"] 

499 ) 

500 LOG.debug("FlashBlade filter %(name)s", {"name": filt}) 

501 name = "{0}.{1}".format(dataset_name, snapshot["id"]) 

502 self._get_flashblade_snapshot_by_name(filt) 

503 fs_attr = purity_fb.FileSystem( 

504 name=dataset_name, source=purity_fb.Reference(name=name) 

505 ) 

506 try: 

507 self._sys.file_systems.create_file_systems( 

508 overwrite=True, 

509 discard_non_snapshotted_data=True, 

510 file_system=fs_attr, 

511 ) 

512 except purity_fb.rest.ApiException as ex: 

513 msg = _("Failed to revert snapshot: %s\n") % ex 

514 LOG.exception(msg) 

515 raise exception.ManilaException(message=msg)