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
« 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
12ANSIBLE_METADATA = {
13 "metadata_version": "1.1",
14 "status": ["preview"],
15 "supported_by": "community",
16}
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"""
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
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
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
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
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
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
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"""
151RETURN = """
152"""
154HAS_PYPURECLIENT = True
155try:
156 from pypureclient.flashblade import (
157 FileSystemExport,
158 FileSystemExportPost,
159 Reference,
160 )
161except ImportError:
162 HAS_PYPURECLIENT = False
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)
177MIN_API_VERSION = "2.17"
180def _context_kwargs(module):
181 """Return context_names kwargs only when a context is set.
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 {}
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.
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]
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 )
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
274def _warn_type_transition(module, blade):
275 """Warn when a same-address export of the OTHER protocol exists.
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 )
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)
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 )
441 module.exit_json(changed=(changed_policy or changed_export))
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)
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 )
475 module = AnsibleModule(argument_spec, supports_check_mode=True)
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)
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)
509if __name__ == "__main__":
510 main()