Coverage for plugins/modules/purefb_export.py: 37%

150 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-10-30 17:06 -0400

1#!/usr/bin/python 

2# -*- coding: utf-8 -*- 

3 

4# (c) 2026, Simon Dodsley (simon@everpuredata.com) 

5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 

6 

7from __future__ import absolute_import, division, print_function 

8 

9__metaclass__ = type 

10 

11 

12ANSIBLE_METADATA = { 

13 "metadata_version": "1.1", 

14 "status": ["preview"], 

15 "supported_by": "community", 

16} 

17 

18 

19DOCUMENTATION = """ 

20--- 

21module: purefb_export 

22version_added: "1.25.0" 

23short_description: Manage filesystem exports on Everpure FlashBlade` 

24description: 

25 - This module manages filesystem exports on Everpure FlashBlade. 

26author: Everpure Ansible Team (@sdodsley) <pure-ansible-team@everpuredata.com> 

27options: 

28 name: 

29 description: 

30 - Export Name. 

31 required: true 

32 type: str 

33 server: 

34 description: 

35 - Name of server to assign export to 

36 type: str 

37 default: "_array_server" 

38 filesystem: 

39 description: 

40 - Filesystem to create the export for. 

41 required: true 

42 type: str 

43 state: 

44 description: 

45 - Create, delete or modifies a filesystem export. 

46 required: false 

47 default: present 

48 type: str 

49 choices: [ "present", "absent" ] 

50 type: 

51 description: 

52 - Type of filesystem export 

53 type: str 

54 choices: [ "NFS", "SMB" ] 

55 default: NFS 

56 export_policy: 

57 description: 

58 - Name of NFS export policy to assign to the export 

59 type: str 

60 share_policy: 

61 description: 

62 - Name of SMB share policy to assign to the export 

63 type: str 

64 client_policy: 

65 description: 

66 - Name of SMB client policy to assign to the export 

67 type: str 

68 rename: 

69 description: 

70 - New name for export 

71 type: str 

72 context: 

73 description: 

74 - Name of fleet member on which to perform the operation. 

75 - This requires the array receiving the request is a member of a fleet 

76 and the context name to be a member of the same fleet. 

77 type: str 

78 default: "" 

79extends_documentation_fragment: 

80 - everpure.flashblade.everpure.fb 

81""" 

82 

83EXAMPLES = """ 

84- name: Create new filesystem NFS export foo for filesystem bar 

85 everpure.flashblade.purefb_export: 

86 name: foo 

87 filesystem: bar 

88 export_policy: acme_1 

89 state: present 

90 fb_url: 10.10.10.2 

91 api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641 

92 

93- name: Create new filesystem NFS export foo for filesystem bar on server test 

94 everpure.flashblade.purefb_export: 

95 name: foo 

96 server: test 

97 filesystem: bar 

98 export_policy: acme_1 

99 state: present 

100 fb_url: 10.10.10.2 

101 api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641 

102 

103- name: Create new filesystem SMB export foo for filesystem bar 

104 everpure.flashblade.purefb_export: 

105 name: foo 

106 filesystem: bar 

107 type: SMB 

108 state: present 

109 fb_url: 10.10.10.2 

110 api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641 

111 

112- name: Delete filesystem export foo on server test 

113 everpure.flashblade.purefb_export: 

114 name: foo 

115 server: test 

116 state: absent 

117 fb_url: 10.10.10.2 

118 api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641 

119 

120- name: Attach NFS export policy to filesystem bar (replaces purefb_fs export_policy) 

121 everpure.flashblade.purefb_export: 

122 name: bar_nfs 

123 filesystem: bar 

124 type: NFS 

125 export_policy: nfs_pol1 

126 state: present 

127 fb_url: 10.10.10.2 

128 api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641 

129 

130- name: Attach SMB share policy to filesystem bar (replaces purefb_fs share_policy) 

131 everpure.flashblade.purefb_export: 

132 name: bar_smb 

133 filesystem: bar 

134 type: SMB 

135 share_policy: smb_share_pol1 

136 state: present 

137 fb_url: 10.10.10.2 

138 api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641 

139 

140- name: Attach SMB client policy to filesystem bar (replaces purefb_fs client_policy) 

141 everpure.flashblade.purefb_export: 

142 name: bar_smb 

143 filesystem: bar 

144 type: SMB 

145 client_policy: smb_client_pol1 

146 state: present 

147 fb_url: 10.10.10.2 

148 api_token: T-55a68eb5-c785-4720-a2ca-8b03903bf641 

149""" 

150 

151RETURN = """ 

152""" 

153 

154HAS_PYPURECLIENT = True 

155try: 

156 from pypureclient.flashblade import ( 

157 FileSystemExport, 

158 FileSystemExportPost, 

159 Reference, 

160 ) 

161except ImportError: 

162 HAS_PYPURECLIENT = False 

163 

164from ansible.module_utils.basic import AnsibleModule 

165from ansible_collections.everpure.flashblade.plugins.module_utils.purefb import ( 

166 get_system, 

167 purefb_argument_spec, 

168) 

169from ansible_collections.everpure.flashblade.plugins.module_utils.version import ( 

170 LooseVersion, 

171) 

172from ansible_collections.everpure.flashblade.plugins.module_utils.common import ( 

173 get_error_message, 

174 get_rest_api_version, 

175) 

176 

177MIN_API_VERSION = "2.17" 

178 

179 

180def _context_kwargs(module): 

181 """Return context_names kwargs only when a context is set. 

182 

183 A standalone array is "not in a fleet" and rejects fleet context, so 

184 only send context_names when a context has actually been provided. 

185 """ 

186 if module.params["context"]: 

187 return {"context_names": [module.params["context"]]} 

188 return {} 

189 

190 

191def _warn_fs_policy_collision(module, blade): 

192 """Warn when the filesystem still carries a legacy filesystem-level 

193 policy on the protocol being touched. 

194 

195 SMB warns even when the caller omits the matching field, because 

196 create_export always writes both client_policy and share_policy on a 

197 new SMB export (defaulting to the built-in allow-everyone policies) 

198 and would otherwise silently shadow the legacy value. 

199 """ 

200 if module.params["type"] == "NFS" and not module.params["export_policy"]: 

201 return 

202 res = blade.get_file_systems( 

203 names=[module.params["filesystem"]], **_context_kwargs(module) 

204 ) 

205 if res.status_code != 200: 

206 return 

207 items = list(res.items) 

208 if not items: 

209 return 

210 fsys = items[0] 

211 

212 nfs = getattr(fsys, "nfs", None) 

213 smb = getattr(fsys, "smb", None) 

214 fs_export_policy = getattr(getattr(nfs, "export_policy", None), "name", None) 

215 fs_share_policy = getattr(getattr(smb, "share_policy", None), "name", None) 

216 fs_client_policy = getattr(getattr(smb, "client_policy", None), "name", None) 

217 if module.params["type"] == "NFS": 

218 if fs_export_policy: 

219 module.warn( 

220 "Filesystem {0} still has filesystem-level " 

221 "nfs.export_policy={1} set. The purefb_export " 

222 "attachment {2} is the surface Purity treats as " 

223 "authoritative; migrate off purefb_fs: export_policy " 

224 "to avoid confusion.".format( 

225 module.params["filesystem"], 

226 fs_export_policy, 

227 module.params["export_policy"], 

228 ) 

229 ) 

230 else: 

231 if fs_share_policy: 

232 module.warn( 

233 "Filesystem {0} still has filesystem-level " 

234 "smb.share_policy={1} set. The export-level " 

235 "share_policy is the surface Purity treats as " 

236 "authoritative and will shadow it; migrate off " 

237 "purefb_fs: share_policy to avoid confusion.".format( 

238 module.params["filesystem"], 

239 fs_share_policy, 

240 ) 

241 ) 

242 if fs_client_policy: 

243 module.warn( 

244 "Filesystem {0} still has filesystem-level " 

245 "smb.client_policy={1} set. The export-level " 

246 "client_policy is the surface Purity treats as " 

247 "authoritative and will shadow it; migrate off " 

248 "purefb_fs: client_policy to avoid confusion.".format( 

249 module.params["filesystem"], 

250 fs_client_policy, 

251 ) 

252 ) 

253 

254 

255def get_export(module, blade): 

256 """Return Filesystem export true name or None""" 

257 filter_string = ( 

258 "export_name='" 

259 + module.params["name"] 

260 + "' and policy_type='" 

261 + module.params["type"] 

262 + "' and member.name='" 

263 + module.params["filesystem"] 

264 + "' and server.name='" 

265 + module.params["server"] 

266 + "'" 

267 ) 

268 res = blade.get_file_system_exports(filter=filter_string, **_context_kwargs(module)) 

269 if res.status_code == 200 and res.total_item_count != 0: 

270 return list(res.items)[0] 

271 return None 

272 

273 

274def _warn_type_transition(module, blade): 

275 """Warn when a same-address export of the OTHER protocol exists. 

276 

277 One export per protocol per filesystem is permitted, so 

278 both can legitimately coexist, but this almost always indicates a 

279 mistyped ``type:`` parameter. 

280 """ 

281 other_type = "SMB" if module.params["type"] == "NFS" else "NFS" 

282 filter_string = ( 

283 "export_name='" 

284 + module.params["name"] 

285 + "' and policy_type='" 

286 + other_type 

287 + "' and member.name='" 

288 + module.params["filesystem"] 

289 + "' and server.name='" 

290 + module.params["server"] 

291 + "'" 

292 ) 

293 res = blade.get_file_system_exports(filter=filter_string, **_context_kwargs(module)) 

294 if res.status_code == 200 and res.total_item_count != 0: 

295 module.warn( 

296 "A {0} export named {1} already exists on filesystem {2} " 

297 "server {3}. Creating a {4} export in addition rather than " 

298 "modifying the existing one. One FileSystemExport per protocol " 

299 "per filesystem is permitted. If this was " 

300 "not intentional, check the `type:` parameter.".format( 

301 other_type, 

302 module.params["name"], 

303 module.params["filesystem"], 

304 module.params["server"], 

305 module.params["type"], 

306 ) 

307 ) 

308 

309 

310def create_export(module, blade): 

311 """Create Filesystem Export""" 

312 changed = True 

313 _warn_fs_policy_collision(module, blade) 

314 if not module.check_mode: 

315 if module.params["type"] == "NFS": 

316 exp_obj = FileSystemExportPost( 

317 export_name=module.params["name"], 

318 server=Reference(name=module.params["server"]), 

319 ) 

320 res = blade.post_file_system_exports( 

321 file_system_export=exp_obj, 

322 member_names=[module.params["filesystem"]], 

323 policy_names=[module.params["export_policy"]], 

324 **_context_kwargs(module), 

325 ) 

326 else: 

327 if not module.params["client_policy"]: 

328 module.params["client_policy"] = "_smb_client_allow_everyone" 

329 if not module.params["share_policy"]: 

330 module.params["share_policy"] = "_smb_share_allow_everyone" 

331 exp_obj = FileSystemExportPost( 

332 export_name=module.params["name"], 

333 server=Reference(name=module.params["server"]), 

334 share_policy=Reference(name=module.params["share_policy"]), 

335 ) 

336 res = blade.post_file_system_exports( 

337 file_system_export=exp_obj, 

338 member_names=[module.params["filesystem"]], 

339 policy_names=[module.params["client_policy"]], 

340 **_context_kwargs(module), 

341 ) 

342 # Only evaluated when a request was made; in check mode there is 

343 # no response to inspect. 

344 if res.status_code != 200: 

345 module.fail_json( 

346 msg="Failed to create export {0} for {1}. Error: {2}".format( 

347 module.params["name"], 

348 module.params["filesystem"], 

349 get_error_message(res), 

350 ) 

351 ) 

352 module.exit_json(changed=changed) 

353 

354 

355def modify_export(module, blade, export): 

356 """Modify Filesystem""" 

357 _warn_fs_policy_collision(module, blade) 

358 changed_export = False 

359 changed_policy = False 

360 if module.params["rename"] and module.params["rename"] != module.params["name"]: 

361 export_name = module.params["rename"] 

362 changed_export = True 

363 else: 

364 export_name = module.params["name"] 

365 if module.params["type"] == "NFS": 

366 if ( 

367 module.params["export_policy"] 

368 and module.params["export_policy"] != export.policy.name 

369 ): 

370 export_policy = module.params["export_policy"] 

371 changed_policy = True 

372 else: 

373 export_policy = export.policy.name 

374 if changed_policy or changed_export: 

375 if not module.check_mode: 

376 exp_obj = FileSystemExport() 

377 if changed_policy and not changed_export: 

378 exp_obj = FileSystemExport(policy=Reference(name=export_policy)) 

379 elif changed_policy and changed_export: 

380 exp_obj = FileSystemExport( 

381 export_name=export_name, policy=Reference(name=export_policy) 

382 ) 

383 elif not changed_policy and changed_export: 

384 exp_obj = FileSystemExport( 

385 export_name=export_name, 

386 ) 

387 res = blade.patch_file_system_exports( 

388 names=[export.name], file_system_export=exp_obj 

389 ) 

390 if res.status_code != 200: 

391 module.fail_json( 

392 msg="Failed to update export {0}. Error: {1}".format( 

393 export.name, get_error_message(res) 

394 ) 

395 ) 

396 else: 

397 if ( 

398 module.params["client_policy"] 

399 and module.params["client_policy"] != export.policy.name 

400 ): 

401 client_policy = module.params["client_policy"] 

402 changed_policy = True 

403 else: 

404 client_policy = export.policy.name 

405 if ( 

406 module.params["share_policy"] 

407 and module.params["share_policy"] != export.share_policy.name 

408 ): 

409 share_policy = module.params["share_policy"] 

410 changed_policy = True 

411 else: 

412 share_policy = export.share_policy.name 

413 if changed_policy or changed_export: 

414 if not module.check_mode: 

415 exp_obj = FileSystemExport() 

416 if changed_policy and not changed_export: 

417 exp_obj = FileSystemExport( 

418 share_policy=Reference(name=share_policy), 

419 policy=Reference(name=client_policy), 

420 ) 

421 elif changed_policy and changed_export: 

422 exp_obj = FileSystemExport( 

423 export_name=export_name, 

424 share_policy=Reference(name=share_policy), 

425 policy=Reference(name=client_policy), 

426 ) 

427 elif not changed_policy and changed_export: 

428 exp_obj = FileSystemExport( 

429 export_name=export_name, 

430 ) 

431 res = blade.patch_file_system_exports( 

432 names=[export.name], file_system_export=exp_obj 

433 ) 

434 if res.status_code != 200: 

435 module.fail_json( 

436 msg="Failed to update export {0}. Error: {1}".format( 

437 export.name, get_error_message(res) 

438 ) 

439 ) 

440 

441 module.exit_json(changed=(changed_policy or changed_export)) 

442 

443 

444def delete_export(module, blade, export): 

445 """Delete Filesystem Export""" 

446 changed = True 

447 if not module.check_mode: 

448 res = blade.delete_file_system_exports(names=[export]) 

449 if res.status_code != 200: 

450 module.fail_json( 

451 msg="Failed to delete export {0}. Error: {1}".format( 

452 export, get_error_message(res) 

453 ) 

454 ) 

455 module.exit_json(changed=changed) 

456 

457 

458def main(): 

459 argument_spec = purefb_argument_spec() 

460 argument_spec.update( 

461 dict( 

462 name=dict(type="str", required=True), 

463 server=dict(type="str", default="_array_server"), 

464 filesystem=dict(type="str", required=True), 

465 rename=dict(type="str"), 

466 type=dict(type="str", choices=["NFS", "SMB"], default="NFS"), 

467 state=dict(default="present", choices=["present", "absent"]), 

468 export_policy=dict(type="str"), 

469 share_policy=dict(type="str"), 

470 client_policy=dict(type="str"), 

471 context=dict(type="str", default=""), 

472 ) 

473 ) 

474 

475 module = AnsibleModule(argument_spec, supports_check_mode=True) 

476 

477 state = module.params["state"] 

478 blade = get_system(module) 

479 api_version = get_rest_api_version(blade) 

480 if ( 

481 LooseVersion(MIN_API_VERSION) <= LooseVersion(api_version) 

482 and not module.params["context"] 

483 ): 

484 # If no context is provided set the context to the local array name 

485 fleet_res = blade.get_fleets() 

486 if fleet_res.status_code == 200 and list(fleet_res.items): 

487 module.params["context"] = list(blade.get_arrays().items)[0].name 

488 server_exists = bool( 

489 blade.get_servers(names=[module.params["server"]]).status_code == 200 

490 ) 

491 if not server_exists: 

492 module.fail_json( 

493 msg="Server {0} does not exist.".format(module.params["server"]) 

494 ) 

495 export = get_export(module, blade) 

496 

497 if state == "present" and not export and not module.params["rename"]: 

498 _warn_type_transition(module, blade) 

499 create_export(module, blade) 

500 elif state == "present" and export: 

501 modify_export(module, blade, export) 

502 elif state == "absent" and export: 

503 delete_export(module, blade, export.name) 

504 elif state == "absent" and not export: 

505 module.exit_json(changed=False) 

506 module.exit_json(changed=False) 

507 

508 

509if __name__ == "__main__": 

510 main()