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
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-30 17:06 -0400
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
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)
7from __future__ import absolute_import, division, print_function
9__metaclass__ = type
11ANSIBLE_METADATA = {
12 "metadata_version": "1.1",
13 "status": ["preview"],
14 "supported_by": "community",
15}
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"""
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
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
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
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
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"""
130RETURN = r"""
131"""
134HAS_PYPURECLIENT = True
135try:
136 from pypureclient.flashblade import (
137 LocalDirectoryService,
138 LocalDirectoryServicePost,
139 )
140except ImportError:
141 HAS_PYPURECLIENT = False
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)
157MIN_REQUIRED_API_VERSION = "2.24"
160def _context_kwargs(module):
161 """Return context_names kwargs when a context is set.
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 {}
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
183def _find_attached_server(module, blade):
184 """Return the name of a server attached to this LDS, or None.
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)
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)
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)
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"]
257 if not patch_kwargs:
258 module.exit_json(changed=False)
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 )
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)
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 )
307 module = AnsibleModule(argument_spec, supports_check_mode=True)
309 if not HAS_PYPURECLIENT:
310 module.fail_json(msg="py-pure-client sdk is required for this module")
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 )
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
328 lds = _get_lds(module, blade)
329 state = module.params["state"]
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)
341if __name__ == "__main__":
342 main()