Coverage for plugins/modules/purefb_mgmt_auth_policy.py: 95%

214 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 

11ANSIBLE_METADATA = { 

12 "metadata_version": "1.1", 

13 "status": ["preview"], 

14 "supported_by": "community", 

15} 

16 

17DOCUMENTATION = r""" 

18--- 

19module: purefb_mgmt_auth_policy 

20version_added: '1.28.0' 

21short_description: Manage FlashBlade Management Authentication Policies and their members 

22description: 

23- Create, update, or delete a FlashBlade Management Authentication Policy. 

24- Management authentication policies define which authentication factors 

25 (password, SSH key/certificate/FIDO2 key) admins must or may present. 

26 Currently the SSH interface is supported. 

27- Reconcile the declarative list of members (admins and/or the array) the 

28 policy is attached to. 

29- A member can be attached to at most one management authentication policy 

30 at a time; the array does not move members between policies implicitly. 

31author: 

32- Pure Storage Ansible Team (@avk) <pure-ansible-team@everpuredata.com> 

33notes: 

34- Changing an attached policy affects subsequent SSH logins for its members 

35 (existing sessions are unaffected). Misconfiguration can lock 

36 administrators out of SSH access - review changes with check mode first. 

37- The built-in C(default-authentication-policy) is not protected by the 

38 array; it can be modified or deleted like any other policy and is the 

39 fallback for members without an explicit policy. Change it with care. 

40- Moving a member between policies (I(replace_existing=true)) is not 

41 atomic on the array; the member is briefly unattached during the move, 

42 and if the move fails midway it is left without a policy until the 

43 task is re-run. 

44- Deletion safety is enforced by this module, not by the array - the 

45 array will delete a policy that still has members attached, silently 

46 detaching them. 

47- When I(state=absent), configuration and membership options are ignored. 

48options: 

49 name: 

50 description: 

51 - Name of the management authentication policy. 

52 type: str 

53 required: true 

54 state: 

55 description: 

56 - Whether the policy should exist. 

57 default: present 

58 choices: [ absent, present ] 

59 type: str 

60 enabled: 

61 description: 

62 - Whether the policy is active. Members of a disabled policy fall back 

63 to the platform default authentication behavior. 

64 - Omit to leave unchanged on an existing policy; defaults to C(true) 

65 when creating a new policy. 

66 type: bool 

67 ssh_allowed_methods: 

68 description: 

69 - Authentication methods where any one method is sufficient for SSH 

70 login. 

71 - Mutually exclusive with I(ssh_required_methods) - a policy uses one 

72 mode or the other. Supplying this list switches the policy to allowed 

73 mode and clears any required methods. 

74 - C(default) permits whatever methods the FlashBlade supports for the 

75 interface and version. 

76 - Omit to leave the current SSH configuration unchanged. 

77 type: list 

78 elements: str 

79 choices: [ password, key, default ] 

80 ssh_required_methods: 

81 description: 

82 - Authentication methods that must all be presented for SSH login. 

83 - Mutually exclusive with I(ssh_allowed_methods) - a policy uses one 

84 mode or the other. Supplying this list switches the policy to required 

85 mode and clears any allowed methods. 

86 - C(default) is not valid as a required method. 

87 - Omit to leave the current SSH configuration unchanged. 

88 type: list 

89 elements: str 

90 choices: [ password, key ] 

91 members: 

92 description: 

93 - Declarative list of members the policy is attached to. 

94 - Setting the list will attach missing members and detach any member 

95 not listed here. 

96 - Omit (or set to C(null)) to leave current membership untouched. 

97 - Provide an empty list to detach every member from the policy. 

98 - Attaching a member that is attached to a different policy fails 

99 unless I(replace_existing=true). 

100 type: list 

101 elements: dict 

102 suboptions: 

103 name: 

104 description: 

105 - Name of the member (a local admin name, or the FlashBlade array 

106 name for I(type=array)). 

107 type: str 

108 required: true 

109 type: 

110 description: 

111 - Type of the member. 

112 type: str 

113 required: true 

114 choices: [ admin, array ] 

115 replace_existing: 

116 description: 

117 - Whether a member currently attached to a different management 

118 authentication policy may be detached from it and attached to this 

119 policy (detach-then-attach). 

120 - When C(false) a conflicting assignment fails the task, naming the 

121 conflicting policy. 

122 type: bool 

123 default: false 

124 context: 

125 description: 

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

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

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

129 type: str 

130 default: "" 

131extends_documentation_fragment: 

132- everpure.flashblade.everpure.fb 

133""" 

134 

135EXAMPLES = r""" 

136- name: Create a policy allowing password or key 

137 everpure.flashblade.purefb_mgmt_auth_policy: 

138 name: ssh-flexible 

139 ssh_allowed_methods: 

140 - password 

141 - key 

142 fb_url: 10.10.10.2 

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

144 

145- name: Create an MFA policy (password AND key) attached to two admins 

146 everpure.flashblade.purefb_mgmt_auth_policy: 

147 name: ssh-mfa 

148 ssh_required_methods: 

149 - password 

150 - key 

151 members: 

152 - name: alice 

153 type: admin 

154 - name: bob 

155 type: admin 

156 fb_url: 10.10.10.2 

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

158 

159- name: Switch an existing policy from required to allowed mode 

160 everpure.flashblade.purefb_mgmt_auth_policy: 

161 name: ssh-mfa 

162 ssh_allowed_methods: 

163 - key 

164 fb_url: 10.10.10.2 

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

166 

167- name: Attach the array baseline, moving it from its current policy 

168 everpure.flashblade.purefb_mgmt_auth_policy: 

169 name: ssh-mfa 

170 members: 

171 - name: my-flashblade 

172 type: array 

173 replace_existing: true 

174 fb_url: 10.10.10.2 

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

176 

177- name: Detach every member from a policy 

178 everpure.flashblade.purefb_mgmt_auth_policy: 

179 name: ssh-mfa 

180 members: [] 

181 fb_url: 10.10.10.2 

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

183 

184- name: Delete a policy (fails while members are attached) 

185 everpure.flashblade.purefb_mgmt_auth_policy: 

186 name: ssh-mfa 

187 state: absent 

188 fb_url: 10.10.10.2 

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

190""" 

191 

192RETURN = r""" 

193""" 

194 

195HAS_PYPURECLIENT = True 

196try: 

197 from pypureclient.flashblade import ( 

198 ManagementAuthenticationPolicy, 

199 ManagementAuthenticationPolicyConfig, 

200 ManagementAuthenticationPolicyPost, 

201 ) 

202except ImportError: 

203 HAS_PYPURECLIENT = False 

204 

205from ansible.module_utils.basic import AnsibleModule 

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

207 get_system, 

208 purefb_argument_spec, 

209) 

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

211 get_error_message, 

212 get_rest_api_version, 

213) 

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

215 LooseVersion, 

216) 

217 

218MIN_MGMT_AUTH_API_VERSION = "2.22" 

219CONTEXT_API_VERSION = "2.17" 

220# Product design exempts these users from authentication-policy membership; 

221# the module must never attach or detach them. 

222PROTECTED_ADMINS = frozenset(["ir", "pureeng"]) 

223# Module member type -> REST member_types value. 

224MEMBER_TYPE_API = {"admin": "admins", "array": "arrays"} 

225MEMBER_TYPE_MODULE = {"admins": "admin", "arrays": "array"} 

226 

227 

228def _ctx(module, api_version): 

229 """Return the context_names kwargs dict when the array supports it.""" 

230 if not module.params.get("context"): 

231 return {} 

232 if LooseVersion(CONTEXT_API_VERSION) <= LooseVersion(api_version): 

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

234 return {} 

235 

236 

237def _validate_methods(module): 

238 """Reject invalid SSH method lists before any API call. 

239 

240 The argument spec enforces per-list choices ('default' is only a choice 

241 of ssh_allowed_methods) and mutual exclusion; this adds the rules the 

242 spec cannot express. 

243 """ 

244 for param in ("ssh_allowed_methods", "ssh_required_methods"): 

245 methods = module.params[param] 

246 if methods is None: 

247 continue 

248 if not methods: 

249 module.fail_json( 

250 msg=( 

251 "{0} must contain at least one method. To switch modes " 

252 "supply the new mode's non-empty list; the other mode is " 

253 "cleared automatically." 

254 ).format(param) 

255 ) 

256 if len(set(methods)) != len(methods): 

257 module.fail_json(msg="Duplicate method in {0}".format(param)) 

258 

259 

260def _validate_members(module): 

261 members = module.params["members"] 

262 if not members: 

263 return 

264 seen = set() 

265 arrays = 0 

266 for member in members: 

267 key = (member["type"], member["name"]) 

268 if key in seen: 

269 module.fail_json( 

270 msg="Duplicate member (name={0}, type={1}) in members".format( 

271 member["name"], member["type"] 

272 ) 

273 ) 

274 seen.add(key) 

275 if member["type"] == "array": 

276 arrays += 1 

277 elif member["name"].lower() in PROTECTED_ADMINS: 

278 module.fail_json( 

279 msg=( 

280 "Admin {0} is exempt from management authentication " 

281 "policies and cannot be attached." 

282 ).format(member["name"]) 

283 ) 

284 if arrays > 1: 

285 module.fail_json(msg="members may contain at most one array member") 

286 

287 

288def _get_policy(module, blade, ctx_kwargs): 

289 """The policy object, or None when it does not exist. 

290 

291 A missing policy is itself a 400 here ("Management authentication 

292 policy does not exist."), so only that error reads as None - any 

293 other failure fails the task rather than masquerading as "absent" 

294 (which would let state=absent report ok on a transient error). 

295 """ 

296 res = blade.get_management_authentication_policies( 

297 names=[module.params["name"]], **ctx_kwargs 

298 ) 

299 if res.status_code == 200: 

300 items = list(res.items) 

301 return items[0] if items else None 

302 error = get_error_message(res) 

303 if error and "policy does not exist" in str(error).lower(): 

304 return None 

305 module.fail_json( 

306 msg="Failed to read policy {0}. Error: {1}".format(module.params["name"], error) 

307 ) 

308 

309 

310def _get_members(module, blade, ctx_kwargs): 

311 """Current membership as a set of (module_type, member_name).""" 

312 res = blade.get_management_authentication_policies_members( 

313 policy_names=[module.params["name"]], **ctx_kwargs 

314 ) 

315 if res.status_code != 200: 

316 module.fail_json( 

317 msg="Failed to list members of policy {0}. Error: {1}".format( 

318 module.params["name"], get_error_message(res) 

319 ) 

320 ) 

321 return set( 

322 (MEMBER_TYPE_MODULE[item.member.resource_type], item.member.name) 

323 for item in res.items 

324 ) 

325 

326 

327def _get_member_current_policy(module, blade, member_type, member_name, ctx_kwargs): 

328 """Name of the policy the member is attached to, or None. 

329 

330 A failed lookup fails the task - treating it as "no conflict" would 

331 silently skip the preflight this call exists for. 

332 """ 

333 res = blade.get_management_authentication_policies_members( 

334 member_names=[member_name], 

335 member_types=[MEMBER_TYPE_API[member_type]], 

336 **ctx_kwargs, 

337 ) 

338 if res.status_code != 200: 

339 module.fail_json( 

340 msg=("Failed to look up current policy for {0} {1}. Error: {2}").format( 

341 member_type, member_name, get_error_message(res) 

342 ) 

343 ) 

344 items = list(res.items) 

345 return items[0].policy.name if items else None 

346 

347 

348def _desired_ssh_config(module): 

349 """Desired (mode, methods-set) from the params, or None when omitted.""" 

350 if module.params["ssh_allowed_methods"] is not None: 

351 return ("allowed", set(module.params["ssh_allowed_methods"])) 

352 if module.params["ssh_required_methods"] is not None: 

353 return ("required", set(module.params["ssh_required_methods"])) 

354 return None 

355 

356 

357def _current_ssh_config(existing): 

358 """Current (mode, methods-set) of a policy. 

359 

360 The array reports the inactive mode as an empty list, so exactly one 

361 mode is non-empty on a configured policy. 

362 """ 

363 allowed = set(getattr(existing.ssh, "allowed_methods", None) or []) 

364 required = set(getattr(existing.ssh, "required_methods", None) or []) 

365 if required: 

366 return ("required", required) 

367 return ("allowed", allowed) 

368 

369 

370def _build_ssh_config(mode, methods): 

371 """SSH config body with the opposite mode explicitly cleared. 

372 

373 PATCH replaces the ssh object wholesale (verified on REST 2.28: an 

374 omitted list is cleared too), but the explicit empty list keeps the 

375 request self-describing and version-safe. 

376 """ 

377 if mode == "allowed": 

378 return ManagementAuthenticationPolicyConfig( 

379 allowed_methods=sorted(methods), required_methods=[] 

380 ) 

381 return ManagementAuthenticationPolicyConfig( 

382 allowed_methods=[], required_methods=sorted(methods) 

383 ) 

384 

385 

386def _warn_if_live(module, blade, ctx_kwargs): 

387 """Warn when changing authentication requirements on an attached policy.""" 

388 if _get_members(module, blade, ctx_kwargs): 

389 module.warn( 

390 "Policy {0} is attached to one or more members. The new " 

391 "authentication requirements apply to their subsequent SSH " 

392 "logins.".format(module.params["name"]) 

393 ) 

394 

395 

396def _validate_members_exist(module, blade, members, ctx_kwargs): 

397 """Fail when a desired member does not exist. 

398 

399 The members relationship GET returns 200 with no items for a 

400 nonexistent member - indistinguishable from "no policy attached" - so 

401 existence has to come from the admins/arrays endpoints. Runs in check 

402 mode too, so check mode catches a mistyped member name. 

403 """ 

404 for member_type, member_name in sorted(members): 

405 if member_type == "admin": 

406 res = blade.get_admins(names=[member_name], **ctx_kwargs) 

407 if res.status_code != 200: 

408 # A missing admin is a 400 "Admin does not exist." here; 

409 # passing the array's error through keeps a transient 

410 # failure from masquerading as a missing member. 

411 module.fail_json( 

412 msg=( 

413 "Member admin {0} does not exist or could not be " 

414 "verified. Error: {1}" 

415 ).format(member_name, get_error_message(res)) 

416 ) 

417 else: 

418 res = blade.get_arrays(**ctx_kwargs) 

419 if res.status_code != 200: 

420 module.fail_json( 

421 msg=( 

422 "Member array {0} could not be verified. " "Error: {1}" 

423 ).format(member_name, get_error_message(res)) 

424 ) 

425 if not any(array.name == member_name for array in res.items): 

426 module.fail_json( 

427 msg="Member array {0} does not exist.".format(member_name) 

428 ) 

429 

430 

431def _find_member_conflicts(module, blade, members, ctx_kwargs): 

432 """Read-only lookup of members attached to a different policy. 

433 

434 Returns a list of (member_type, member_name, other_policy_name). 

435 """ 

436 conflicts = [] 

437 for member_type, member_name in sorted(members): 

438 other = _get_member_current_policy( 

439 module, blade, member_type, member_name, ctx_kwargs 

440 ) 

441 if other and other != module.params["name"]: 

442 conflicts.append((member_type, member_name, other)) 

443 return conflicts 

444 

445 

446def _fail_on_member_conflicts(module, conflicts): 

447 if conflicts and not module.params["replace_existing"]: 

448 module.fail_json( 

449 msg=( 

450 "Cannot attach members already attached to another " 

451 "management authentication policy: {0}. Set " 

452 "replace_existing=true to move them." 

453 ).format( 

454 ", ".join( 

455 "{0} {1} (attached to {2})".format(mtype, mname, other) 

456 for mtype, mname, other in conflicts 

457 ) 

458 ) 

459 ) 

460 

461 

462def reconcile_members(module, blade, ctx_kwargs): 

463 """Diff desired members against actual; attach/detach to converge. 

464 

465 Returns True if any change was made (or would be made in check-mode). 

466 """ 

467 desired_list = module.params["members"] 

468 if desired_list is None: 

469 return False 

470 

471 desired = set((member["type"], member["name"]) for member in desired_list) 

472 current = _get_members(module, blade, ctx_kwargs) 

473 

474 to_add = desired - current 

475 to_remove = set( 

476 member 

477 for member in current - desired 

478 # Never detach exempt users, even if the array reports them. 

479 if not (member[0] == "admin" and member[1].lower() in PROTECTED_ADMINS) 

480 ) 

481 

482 # Existence and conflict checks run in check mode too so that check 

483 # mode fails exactly where a real run would. 

484 _validate_members_exist(module, blade, to_add, ctx_kwargs) 

485 conflicts = _find_member_conflicts(module, blade, to_add, ctx_kwargs) 

486 _fail_on_member_conflicts(module, conflicts) 

487 

488 if not to_add and not to_remove: 

489 return False 

490 

491 if module.check_mode: 

492 return True 

493 

494 for member_type, member_name in sorted(to_remove): 

495 res = blade.delete_management_authentication_policies_members( 

496 policy_names=[module.params["name"]], 

497 member_names=[member_name], 

498 member_types=[MEMBER_TYPE_API[member_type]], 

499 **ctx_kwargs, 

500 ) 

501 if res.status_code != 200: 

502 module.fail_json( 

503 msg=("Failed to detach {0} {1} from policy {2}. Error: {3}").format( 

504 member_type, 

505 member_name, 

506 module.params["name"], 

507 get_error_message(res), 

508 ) 

509 ) 

510 

511 # Attach each member as a tight detach->attach pair. The array offers 

512 # no atomic move (member-side POSTs refuse an attached member just 

513 # like the policy-side one), so a moved member is unavoidably 

514 # unattached between its own two calls - but only between those two, 

515 # and a failure orphans at most that one member. 

516 conflict_by_member = {(mtype, mname): other for mtype, mname, other in conflicts} 

517 for member_type, member_name in sorted(to_add): 

518 other = conflict_by_member.get((member_type, member_name)) 

519 if other: 

520 res = blade.delete_management_authentication_policies_members( 

521 policy_names=[other], 

522 member_names=[member_name], 

523 member_types=[MEMBER_TYPE_API[member_type]], 

524 **ctx_kwargs, 

525 ) 

526 if res.status_code != 200: 

527 module.fail_json( 

528 msg=("Failed to detach {0} {1} from policy {2}. Error: {3}").format( 

529 member_type, member_name, other, get_error_message(res) 

530 ) 

531 ) 

532 res = blade.post_management_authentication_policies_members( 

533 policy_names=[module.params["name"]], 

534 member_names=[member_name], 

535 member_types=[MEMBER_TYPE_API[member_type]], 

536 **ctx_kwargs, 

537 ) 

538 if res.status_code != 200: 

539 orphan_note = "" 

540 if other: 

541 orphan_note = ( 

542 " {0} {1} is currently attached to no policy; re-run " 

543 "the task to attach it." 

544 ).format(member_type, member_name) 

545 module.fail_json( 

546 msg=("Failed to attach {0} {1} to policy {2}. Error: {3}{4}").format( 

547 member_type, 

548 member_name, 

549 module.params["name"], 

550 get_error_message(res), 

551 orphan_note, 

552 ) 

553 ) 

554 

555 return True 

556 

557 

558def delete_policy(module, blade, ctx_kwargs): 

559 """Guarded delete. 

560 

561 The array allows deleting an attached policy, so the members check 

562 must live client-side. 

563 """ 

564 if _get_members(module, blade, ctx_kwargs): 

565 module.fail_json( 

566 msg=( 

567 "Authentication policy {0} has attached members. Detach " 

568 "members first (members: [] with state: present)." 

569 ).format(module.params["name"]) 

570 ) 

571 if not module.check_mode: 

572 res = blade.delete_management_authentication_policies( 

573 names=[module.params["name"]], **ctx_kwargs 

574 ) 

575 if res.status_code != 200: 

576 module.fail_json( 

577 msg="Failed to delete policy {0}. Error: {1}".format( 

578 module.params["name"], get_error_message(res) 

579 ) 

580 ) 

581 module.exit_json(changed=True) 

582 

583 

584def create_policy(module, blade, ctx_kwargs): 

585 desired_ssh = _desired_ssh_config(module) 

586 if desired_ssh is None: 

587 module.fail_json( 

588 msg=( 

589 "Creating a policy requires ssh_allowed_methods or " 

590 "ssh_required_methods." 

591 ) 

592 ) 

593 enabled = module.params["enabled"] 

594 if enabled is None: 

595 enabled = True 

596 

597 # Preflight member existence and conflicts before the policy POST so 

598 # a refused attach cannot leave a half-created policy behind, and so 

599 # check mode fails exactly where a real run would. 

600 if module.params["members"]: 

601 desired_members = set( 

602 (member["type"], member["name"]) for member in module.params["members"] 

603 ) 

604 _validate_members_exist(module, blade, desired_members, ctx_kwargs) 

605 _fail_on_member_conflicts( 

606 module, 

607 _find_member_conflicts(module, blade, desired_members, ctx_kwargs), 

608 ) 

609 

610 if module.check_mode: 

611 module.exit_json(changed=True) 

612 

613 res = blade.post_management_authentication_policies( 

614 names=[module.params["name"]], 

615 policy=ManagementAuthenticationPolicyPost( 

616 enabled=enabled, 

617 ssh=_build_ssh_config(*desired_ssh), 

618 ), 

619 **ctx_kwargs, 

620 ) 

621 if res.status_code != 200: 

622 module.fail_json( 

623 msg="Failed to create policy {0}. Error: {1}".format( 

624 module.params["name"], get_error_message(res) 

625 ) 

626 ) 

627 reconcile_members(module, blade, ctx_kwargs) 

628 module.exit_json(changed=True) 

629 

630 

631def update_policy(module, blade, ctx_kwargs, existing): 

632 changed = False 

633 

634 patch_kwargs = {} 

635 if ( 

636 module.params["enabled"] is not None 

637 and module.params["enabled"] != existing.enabled 

638 ): 

639 patch_kwargs["enabled"] = module.params["enabled"] 

640 

641 desired_ssh = _desired_ssh_config(module) 

642 if desired_ssh is not None and desired_ssh != _current_ssh_config(existing): 

643 patch_kwargs["ssh"] = _build_ssh_config(*desired_ssh) 

644 

645 if patch_kwargs: 

646 changed = True 

647 _warn_if_live(module, blade, ctx_kwargs) 

648 if not module.check_mode: 

649 res = blade.patch_management_authentication_policies( 

650 names=[module.params["name"]], 

651 policy=ManagementAuthenticationPolicy(**patch_kwargs), 

652 **ctx_kwargs, 

653 ) 

654 if res.status_code != 200: 

655 module.fail_json( 

656 msg="Failed to update policy {0}. Error: {1}".format( 

657 module.params["name"], get_error_message(res) 

658 ) 

659 ) 

660 

661 if reconcile_members(module, blade, ctx_kwargs): 

662 changed = True 

663 

664 module.exit_json(changed=changed) 

665 

666 

667def main(): 

668 argument_spec = purefb_argument_spec() 

669 argument_spec.update( 

670 dict( 

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

672 state=dict(type="str", default="present", choices=["absent", "present"]), 

673 enabled=dict(type="bool"), 

674 ssh_allowed_methods=dict( 

675 type="list", 

676 elements="str", 

677 choices=["password", "key", "default"], 

678 ), 

679 ssh_required_methods=dict( 

680 type="list", 

681 elements="str", 

682 choices=["password", "key"], 

683 ), 

684 members=dict( 

685 type="list", 

686 elements="dict", 

687 options=dict( 

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

689 type=dict( 

690 type="str", 

691 required=True, 

692 choices=["admin", "array"], 

693 ), 

694 ), 

695 ), 

696 replace_existing=dict(type="bool", default=False), 

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

698 ) 

699 ) 

700 

701 module = AnsibleModule( 

702 argument_spec, 

703 mutually_exclusive=[["ssh_allowed_methods", "ssh_required_methods"]], 

704 supports_check_mode=True, 

705 ) 

706 

707 if not HAS_PYPURECLIENT: 

708 module.fail_json(msg="py-pure-client sdk is required for this module") 

709 

710 state = module.params["state"] 

711 if state == "present": 

712 _validate_methods(module) 

713 _validate_members(module) 

714 

715 blade = get_system(module) 

716 api_version = get_rest_api_version(blade) 

717 if LooseVersion(MIN_MGMT_AUTH_API_VERSION) > LooseVersion(api_version): 

718 module.fail_json( 

719 msg=( 

720 "FlashBlade REST version {0} does not support management " 

721 "authentication policies (requires {1}+)." 

722 ).format(api_version, MIN_MGMT_AUTH_API_VERSION) 

723 ) 

724 ctx_kwargs = _ctx(module, api_version) 

725 

726 existing = _get_policy(module, blade, ctx_kwargs) 

727 

728 if state == "absent": 

729 if existing is None: 

730 module.exit_json(changed=False) 

731 delete_policy(module, blade, ctx_kwargs) 

732 else: 

733 if existing is None: 

734 create_policy(module, blade, ctx_kwargs) 

735 else: 

736 update_policy(module, blade, ctx_kwargs, existing) 

737 

738 

739if __name__ == "__main__": 

740 main()