Coverage for plugins/modules/purefb_local_ds.py: 94%

104 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_local_ds 

20version_added: '1.28.0' 

21short_description: Manage FlashBlade Local Directory Services 

22description: 

23- Create, update or delete a FlashBlade Local Directory Service (LDS). 

24- An LDS is the server-scoped container that owns local users and local 

25 groups (see M(everpure.flashblade.purefb_local_user) and 

26 M(everpure.flashblade.purefb_local_group)). 

27- Requires FlashBlade REST API version 2.24 or later (Purity//FB 4.6.8+). 

28- Server attachment is not managed here; attach an existing LDS to a server 

29 via M(everpure.flashblade.purefb_server). Rename and the destroyed / 

30 eradicate lifecycle are deferred until validated against a live 2.24 array. 

31author: 

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

33options: 

34 name: 

35 description: 

36 - Name of the Local Directory Service. 

37 - Accepts the realm-qualified form C(realm::name) for realm-scoped LDSes. 

38 type: str 

39 required: true 

40 state: 

41 description: 

42 - Whether the LDS should exist or not. 

43 - I(state=absent) issues a direct DELETE. The module preflight-checks 

44 C(/servers) and refuses to delete an LDS still attached to any server; 

45 detach it via M(everpure.flashblade.purefb_server) first. 

46 type: str 

47 default: present 

48 choices: [ absent, present ] 

49 domain: 

50 description: 

51 - Domain string for the LDS. 

52 - Optional at create - when omitted, the array defaults the domain to 

53 the LDS name. 

54 - Patchable, but a domain change is disruptive; the API refuses it while 

55 the LDS is attached to a server. The module warns before issuing the 

56 PATCH in that case and surfaces the API error verbatim if rejected. 

57 - When omitted from the play on an existing LDS, the module does not 

58 touch the domain field (never "reset to LDS-name-default"). 

59 type: str 

60 context: 

61 description: 

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

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

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

65 type: str 

66 default: "" 

67extends_documentation_fragment: 

68- everpure.flashblade.everpure.fb 

69""" 

70 

71EXAMPLES = r""" 

72- name: Create a local directory service with an explicit domain 

73 everpure.flashblade.purefb_local_ds: 

74 name: myserver_local 

75 domain: local 

76 fb_url: 10.10.10.2 

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

78 

79- name: Create a local directory service, letting the array default the domain 

80 everpure.flashblade.purefb_local_ds: 

81 name: myserver_local 

82 fb_url: 10.10.10.2 

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

84 

85- name: Change the domain on an existing LDS (must be detached from any server) 

86 everpure.flashblade.purefb_local_ds: 

87 name: myserver_local 

88 domain: corp.local 

89 fb_url: 10.10.10.2 

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

91 

92- name: Delete a local directory service (must be detached first) 

93 everpure.flashblade.purefb_local_ds: 

94 name: myserver_local 

95 state: absent 

96 fb_url: 10.10.10.2 

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

98 

99- name: Full workflow - create LDS, attach via purefb_server, add a user 

100 block: 

101 - name: Create LDS 

102 everpure.flashblade.purefb_local_ds: 

103 name: myserver_local 

104 domain: local 

105 fb_url: 10.10.10.2 

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

107 - name: Attach LDS to server (see purefb_server for details) 

108 everpure.flashblade.purefb_server: 

109 name: myserver 

110 directory_service: 

111 - myserver_local 

112 fb_url: 10.10.10.2 

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

114 - name: Create primary group for alice 

115 everpure.flashblade.purefb_local_group: 

116 fb_url: 10.10.10.2 

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

118 name: alice 

119 local_directory_service: myserver_local 

120 - name: Create local user in the LDS 

121 everpure.flashblade.purefb_local_user: 

122 fb_url: 10.10.10.2 

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

124 name: alice 

125 local_directory_service: myserver_local 

126 password: "s3cret!" 

127 primary_group: alice 

128""" 

129 

130RETURN = r""" 

131""" 

132 

133 

134HAS_PYPURECLIENT = True 

135try: 

136 from pypureclient.flashblade import ( 

137 LocalDirectoryService, 

138 LocalDirectoryServicePost, 

139 ) 

140except ImportError: 

141 HAS_PYPURECLIENT = False 

142 

143 

144from ansible.module_utils.basic import AnsibleModule 

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

146 get_system, 

147 purefb_argument_spec, 

148) 

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

150 get_error_message, 

151 get_rest_api_version, 

152) 

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

154 LooseVersion, 

155) 

156 

157MIN_REQUIRED_API_VERSION = "2.24" 

158 

159 

160def _context_kwargs(module): 

161 """Return context_names kwargs when a context is set. 

162 

163 LDS requires REST 2.24, which is well above the 2.17 fleet-context floor, 

164 so the array is guaranteed to accept context_names when we reach here. 

165 """ 

166 if module.params["context"]: 

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

168 return {} 

169 

170 

171def _get_lds(module, blade): 

172 ctx = _context_kwargs(module) 

173 res = blade.get_directory_services_local_directory_services( 

174 names=[module.params["name"]], **ctx 

175 ) 

176 if res.status_code == 200: 

177 items = list(res.items) 

178 if items: 

179 return items[0] 

180 return None 

181 

182 

183def _find_attached_server(module, blade): 

184 """Return the name of a server attached to this LDS, or None. 

185 

186 Fails the module if the attachment check itself errors, so the delete 

187 preflight can never silently pass on an API failure. 

188 """ 

189 ctx = _context_kwargs(module) 

190 filter_expr = "local_directory_service.name='{0}'".format(module.params["name"]) 

191 res = blade.get_servers(filter=filter_expr, **ctx) 

192 if res.status_code != 200: 

193 module.fail_json( 

194 msg=( 

195 "Failed to check server attachment for " 

196 "local directory service {0}. Error: {1}" 

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

198 ) 

199 items = list(res.items) 

200 if not items: 

201 return None 

202 return getattr(items[0], "name", None) 

203 

204 

205def delete_lds(module, blade): 

206 attached = _find_attached_server(module, blade) 

207 if attached: 

208 module.fail_json( 

209 msg=( 

210 "Local directory service '{0}' is attached to server '{1}'. " 

211 "Detach it via purefb_server before deleting." 

212 ).format(module.params["name"], attached) 

213 ) 

214 changed = True 

215 if not module.check_mode: 

216 ctx = _context_kwargs(module) 

217 res = blade.delete_directory_services_local_directory_services( 

218 names=[module.params["name"]], **ctx 

219 ) 

220 if res.status_code != 200: 

221 module.fail_json( 

222 msg="Failed to delete local directory service {0}. Error: {1}".format( 

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

224 ) 

225 ) 

226 module.exit_json(changed=changed) 

227 

228 

229def create_lds(module, blade): 

230 changed = True 

231 if not module.check_mode: 

232 if module.params["domain"] is not None: 

233 body = LocalDirectoryServicePost(domain=module.params["domain"]) 

234 else: 

235 body = LocalDirectoryServicePost() 

236 ctx = _context_kwargs(module) 

237 res = blade.post_directory_services_local_directory_services( 

238 names=[module.params["name"]], local_directory=body, **ctx 

239 ) 

240 if res.status_code != 200: 

241 module.fail_json( 

242 msg="Failed to create local directory service {0}. Error: {1}".format( 

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

244 ) 

245 ) 

246 module.exit_json(changed=changed) 

247 

248 

249def update_lds(module, blade, lds): 

250 ctx = _context_kwargs(module) 

251 patch_kwargs = {} 

252 if module.params["domain"] is not None and module.params["domain"] != getattr( 

253 lds, "domain", None 

254 ): 

255 patch_kwargs["domain"] = module.params["domain"] 

256 

257 if not patch_kwargs: 

258 module.exit_json(changed=False) 

259 

260 attached_server = getattr(lds, "server", None) 

261 attached_name = getattr(attached_server, "name", None) if attached_server else None 

262 if attached_name: 

263 module.warn( 

264 ( 

265 "Changing domain on local directory service '{0}' is disruptive " 

266 "and is only permitted when detached; the array may reject this " 

267 "while '{0}' is attached to server '{1}'." 

268 ).format(module.params["name"], attached_name) 

269 ) 

270 

271 changed = True 

272 if not module.check_mode: 

273 res = blade.patch_directory_services_local_directory_services( 

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

275 local_directory=LocalDirectoryService(**patch_kwargs), 

276 **ctx, 

277 ) 

278 if res.status_code != 200: 

279 if attached_name: 

280 module.fail_json( 

281 msg=( 

282 "Failed to update local directory service {0} " 

283 "(attached to server '{1}'). Error: {2}" 

284 ).format( 

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

286 ) 

287 ) 

288 module.fail_json( 

289 msg="Failed to update local directory service {0}. Error: {1}".format( 

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

291 ) 

292 ) 

293 module.exit_json(changed=changed) 

294 

295 

296def main(): 

297 argument_spec = purefb_argument_spec() 

298 argument_spec.update( 

299 dict( 

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

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

302 domain=dict(type="str"), 

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

304 ) 

305 ) 

306 

307 module = AnsibleModule(argument_spec, supports_check_mode=True) 

308 

309 if not HAS_PYPURECLIENT: 

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

311 

312 blade = get_system(module) 

313 api_version = get_rest_api_version(blade) 

314 if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(api_version): 

315 module.fail_json( 

316 msg=( 

317 "FlashBlade REST version not supported. " 

318 "Minimum version required: {0} (Purity//FB 4.6.8+)" 

319 ).format(MIN_REQUIRED_API_VERSION) 

320 ) 

321 

322 if not module.params["context"]: 

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

324 fleet_res = blade.get_fleets() 

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

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

327 

328 lds = _get_lds(module, blade) 

329 state = module.params["state"] 

330 

331 if state == "absent" and lds: 

332 delete_lds(module, blade) 

333 elif state == "absent": 

334 module.exit_json(changed=False) 

335 elif state == "present" and not lds: 

336 create_lds(module, blade) 

337 else: 

338 update_lds(module, blade, lds) 

339 

340 

341if __name__ == "__main__": 

342 main()