From 01f4e3e060681117359853c6049b1a02fb5b7362 Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Thu, 25 Jun 2026 17:04:16 -0400 Subject: [PATCH 1/3] Add purefa_zte module for Zero Touch Erasure Adds a new purefa_zte module that performs Zero Touch Erasure (ZTE) on a FlashArray using the /arrays/erasures REST endpoints (post/get/patch/delete). The module exposes the three documented ZTE phases via a state parameter: - start - Phase 1, securely wipe drives and generate the sanitization certificate (post_arrays_erasures) - status - report process status and return the sanitization certificate (get_arrays_erasures), non-destructive, the default - finalize - Phase 3, finalize the reset, optionally reinstalling the image (patch_arrays_erasures) - cancel - cancel a failed reset (delete_arrays_erasures) Destructive phases require an explicit eradicate=true safety acknowledgement, the module supports check_mode, gates on REST API 2.34, and is registered in meta/runtime.yml. Co-Authored-By: Claude Opus 4.8 --- README.md | 1 + changelogs/fragments/1015_add_purefa_zte.yaml | 2 + meta/runtime.yml | 1 + plugins/modules/purefa_zte.py | 383 ++++++++++++++++++ 4 files changed, 387 insertions(+) create mode 100644 changelogs/fragments/1015_add_purefa_zte.yaml create mode 100644 plugins/modules/purefa_zte.py diff --git a/README.md b/README.md index a9dfbd43..ed7ad13a 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ All modules are idempotent with the exception of modules that change or set pass - purefa_volume - manage volumes on the FlashArray - purefa_volume_tags - manage volume tags on the FlashArray - purefa_workload - manage Fusion workloads in a Fleet +- purefa_zte - perform Zero Touch Erasure (ZTE) factory reset on the FlashArray ## License Information diff --git a/changelogs/fragments/1015_add_purefa_zte.yaml b/changelogs/fragments/1015_add_purefa_zte.yaml new file mode 100644 index 00000000..3ae63f7d --- /dev/null +++ b/changelogs/fragments/1015_add_purefa_zte.yaml @@ -0,0 +1,2 @@ +minor_changes: + - purefa_zte - New module to perform Zero Touch Erasure (ZTE) factory reset on a FlashArray diff --git a/meta/runtime.yml b/meta/runtime.yml index 5e44ba6a..622152de 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -70,6 +70,7 @@ action_groups: - purefa_volume - purefa_volume_tags - purefa_workload + - purefa_zte plugin_routing: modules: purefa_sso: diff --git a/plugins/modules/purefa_zte.py b/plugins/modules/purefa_zte.py new file mode 100644 index 00000000..82b3b568 --- /dev/null +++ b/plugins/modules/purefa_zte.py @@ -0,0 +1,383 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2026, Simon Dodsley (simon@purestorage.com) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = r""" +--- +module: purefa_zte +version_added: '1.44.0' +short_description: Perform Zero Touch Erasure (ZTE) on a FlashArray +description: +- Securely reset a FlashArray to a factory-fresh state using Zero Touch + Erasure (ZTE). +- ZTE wipes all drives, generates a NIST SP800-88R1 sanitization certificate + and optionally reinstalls the array image. +- B(This is an extremely destructive and irreversible operation.) All data on + the array is permanently and unrecoverably erased. +- Requires Purity//FA 6.6.8, or higher. +- All customer data (pods, protection groups, volumes/snapshots, array + connections, file systems, directory service, file local users and groups, + and Active Directory service) must be deleted and eradicated before starting + ZTE, otherwise the operation will fail immediately. +- ZTE is a multi-phase process. Use I(state=start) to begin the drive wipe, + I(state=status) to monitor progress and retrieve the sanitization + certificate, and I(state=finalize) to complete the reset. Use + I(state=cancel) to cancel a failed reset. +- During the drive wipe the array REST API service is unavailable for a period + of time (typically around 30 minutes). This is expected behaviour. +author: +- Everpure Ansible Team (@sdodsley) +options: + state: + description: + - The ZTE phase to perform. + - I(start) begins Phase 1, securely wiping the drives and generating the + sanitization certificate. + - I(status) returns the current ZTE process status and, when available, + the sanitization certificate. This is non-destructive. + - I(finalize) performs Phase 3, finalizing the reset. This permanently + deletes the sanitization certificate, so ensure it has been saved first. + - I(cancel) cancels a ZTE process that is in a failed state. + type: str + choices: [ start, status, finalize, cancel ] + default: status + eradicate: + description: + - A safety acknowledgement that must be set to C(true) to perform any + destructive ZTE phase (I(start) or I(finalize)). + - Setting this to C(true) confirms that you understand all data on the + array will be permanently and unrecoverably erased. + type: bool + default: false + preserve_config: + description: + - Whether to preserve array configuration data during the drive wipe. + - Only used when I(state=start). + type: bool + default: true + skip_phonehome_check: + description: + - Skip the phonehome connectivity check when starting ZTE. + - Must be set to C(true) for darksite arrays that do not have connectivity + to Pure Storage cloud servers, otherwise the reset will fail the + phonehome connectivity check. + - Only used when I(state=start). + type: bool + default: false + reinstall_image: + description: + - Whether to reinstall the array image when finalizing ZTE. + - When C(false) (Option 1), existing system-generated data, management + interface configuration and configuration data are retained. + - When C(true) (Option 2), the array image is reinstalled and existing + system-generated data, management interface configuration and + configuration data are removed. + - Only used when I(state=finalize). + type: bool + default: false + image_source: + description: + - The source of the image used when I(reinstall_image=true). + - For phoning-home arrays set this to C(auto). + - For darksite arrays set this to the URL, or file path, of an image + bundle that the array can access. The image bundle must be the same + version as the Purity//FA version running on the array. + - Only used when I(state=finalize) and I(reinstall_image=true). + type: str + default: auto + image_version: + description: + - The Purity//FA version of the darksite image bundle referenced by + I(image_source). + - Only used when I(state=finalize) and I(reinstall_image=true) for a + darksite array. + type: str +extends_documentation_fragment: +- purestorage.flasharray.purestorage.fa +""" + +EXAMPLES = r""" +- name: Check current ZTE status and retrieve sanitization certificate + purestorage.flasharray.purefa_zte: + state: status + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + register: zte + +- name: Save the sanitization certificate before finalizing + ansible.builtin.copy: + content: "{{ zte.zte.sanitization_certificate }}" + dest: ./sanitization_certificate.txt + when: zte.zte.sanitization_certificate | length > 0 + +- name: Start ZTE (Phase 1 - wipe drives) + purestorage.flasharray.purefa_zte: + state: start + eradicate: true + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Start ZTE on a darksite array + purestorage.flasharray.purefa_zte: + state: start + eradicate: true + skip_phonehome_check: true + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Finalize ZTE without reinstalling the image (Option 1) + purestorage.flasharray.purefa_zte: + state: finalize + eradicate: true + reinstall_image: false + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Finalize ZTE and reinstall image on a phoning-home array (Option 2) + purestorage.flasharray.purefa_zte: + state: finalize + eradicate: true + reinstall_image: true + image_source: auto + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Finalize ZTE and reinstall image on a darksite array (Option 2) + purestorage.flasharray.purefa_zte: + state: finalize + eradicate: true + reinstall_image: true + image_source: "https://server.example.com/purity_iso.sh" + image_version: "6.6.8" + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 + +- name: Cancel a failed ZTE process + purestorage.flasharray.purefa_zte: + state: cancel + fa_url: 10.10.10.2 + api_token: e31060a7-21fc-e277-6240-25983c6c4592 +""" + +RETURN = r""" +zte: + description: The current state of the ZTE process. + returned: always + type: dict + contains: + status: + description: + - The status of the ZTE process, for example C(resetting), + C(waiting_for_finalize), C(downloading), C(reset_failed), + C(download_failed), C(reimage_failed) or C(finalized). + - An empty string indicates that no ZTE process is in progress. + type: str + returned: always + details: + description: Failure information when the ZTE process has failed. + type: str + returned: always + image_download_progress: + description: The image download progress when reinstalling the image. + type: str + returned: always + sanitization_certificate: + description: + - The NIST SP800-88R1 sanitization certificate generated during the + drive wipe. + - This is only available between Phase 1 completing and ZTE being + finalized. It cannot be retrieved once ZTE is finalized. + type: str + returned: always +""" + +HAS_PURESTORAGE = True +try: + from pypureclient import flasharray +except ImportError: + HAS_PURESTORAGE = False + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( + get_array, + purefa_argument_spec, +) +from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( + LooseVersion, +) +from ansible_collections.purestorage.flasharray.plugins.module_utils.api_helpers import ( + check_response, +) + +MIN_REQUIRED_API_VERSION = "2.34" + +# Statuses that indicate a ZTE process is actively running or awaiting action +ACTIVE_STATES = frozenset( + [ + "resetting", + "waiting_for_finalize", + "downloading", + "downloaded", + ] +) + + +def get_erasure(array): + """Return the current erasure (factory reset) object, or None. + + A successful Option 1 finalize leaves no erasure in progress, in which case + ``get_arrays_erasures`` returns an empty item list. + """ + res = array.get_arrays_erasures() + if res.status_code != 200: + return None + items = list(res.items) + return items[0] if items else None + + +def erasure_facts(current): + """Build the return dict from an erasure object.""" + if current is None: + return { + "status": "", + "details": "", + "image_download_progress": "", + "sanitization_certificate": "", + } + return { + "status": getattr(current, "status", "") or "", + "details": getattr(current, "details", "") or "", + "image_download_progress": getattr(current, "image_download_progress", "") + or "", + "sanitization_certificate": getattr(current, "sanitization_certificate", "") + or "", + } + + +def zte_status(module, array, current): + """Report the current ZTE status. Non-destructive.""" + module.exit_json(changed=False, zte=erasure_facts(current)) + + +def start_zte(module, array, current): + """Phase 1 - securely wipe the drives and generate the sanitization cert.""" + if not module.params["eradicate"]: + module.fail_json( + msg="To start ZTE the `eradicate` parameter must be set to true. " + "This permanently and unrecoverably erases all data on the array." + ) + if current is not None and getattr(current, "status", "") in ACTIVE_STATES: + # A reset is already in progress - nothing to do + module.exit_json(changed=False, zte=erasure_facts(current)) + changed = True + if not module.check_mode: + preserve = ["all"] if module.params["preserve_config"] else [] + res = array.post_arrays_erasures( + eradicate_all_data=True, + preserve_configuration_data=preserve, + skip_phonehome_check=module.params["skip_phonehome_check"], + ) + check_response(res, module, "Failed to start ZTE") + current = get_erasure(array) + module.exit_json(changed=changed, zte=erasure_facts(current)) + + +def finalize_zte(module, array, current): + """Phase 3 - finalize the reset.""" + if not module.params["eradicate"]: + module.fail_json( + msg="To finalize ZTE the `eradicate` parameter must be set to true. " + "This permanently deletes the sanitization certificate." + ) + if current is None: + module.fail_json(msg="There is no ZTE process to finalize") + changed = True + if not module.check_mode: + kwargs = dict( + finalize=True, + eradicate_all_data=True, + delete_sanitization_certificate=True, + reinstall_image=module.params["reinstall_image"], + ) + if module.params["reinstall_image"]: + kwargs["erasure_patch"] = flasharray.ArrayErasurePatch( + image_source=module.params["image_source"], + image_version=module.params["image_version"], + ) + res = array.patch_arrays_erasures(**kwargs) + check_response(res, module, "Failed to finalize ZTE") + current = get_erasure(array) + module.exit_json(changed=changed, zte=erasure_facts(current)) + + +def cancel_zte(module, array, current): + """Cancel a ZTE process that is in a failed state.""" + if current is None: + module.exit_json(changed=False, zte=erasure_facts(current)) + changed = True + if not module.check_mode: + res = array.delete_arrays_erasures() + check_response(res, module, "Failed to cancel ZTE") + current = get_erasure(array) + module.exit_json(changed=changed, zte=erasure_facts(current)) + + +def main(): + argument_spec = purefa_argument_spec() + argument_spec.update( + dict( + state=dict( + type="str", + default="status", + choices=["start", "status", "finalize", "cancel"], + ), + eradicate=dict(type="bool", default=False), + preserve_config=dict(type="bool", default=True), + skip_phonehome_check=dict(type="bool", default=False), + reinstall_image=dict(type="bool", default=False), + image_source=dict(type="str", default="auto"), + image_version=dict(type="str"), + ) + ) + + module = AnsibleModule(argument_spec, supports_check_mode=True) + + if not HAS_PURESTORAGE: + module.fail_json(msg="py-pure-client sdk is required for this module") + + array = get_array(module) + api_version = array.get_rest_version() + + if LooseVersion(MIN_REQUIRED_API_VERSION) > LooseVersion(api_version): + module.fail_json( + msg="FlashArray REST version not supported. " + "Minimum version required: {0}".format(MIN_REQUIRED_API_VERSION) + ) + + current = get_erasure(array) + + state = module.params["state"] + if state == "start": + start_zte(module, array, current) + elif state == "finalize": + finalize_zte(module, array, current) + elif state == "cancel": + cancel_zte(module, array, current) + else: + zte_status(module, array, current) + + +if __name__ == "__main__": + main() From a0f547f26ce46a53cd090f80cf826518ac9c1bf9 Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Fri, 17 Jul 2026 11:46:05 -0400 Subject: [PATCH 2/3] Update module references from 'purestorage' to 'everpure' --- plugins/modules/purefa_zte.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/modules/purefa_zte.py b/plugins/modules/purefa_zte.py index 82b3b568..d7e5a927 100644 --- a/plugins/modules/purefa_zte.py +++ b/plugins/modules/purefa_zte.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2026, Simon Dodsley (simon@purestorage.com) +# (c) 2026, Simon Dodsley (simon@everpuredata.com) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function @@ -38,7 +38,7 @@ - During the drive wipe the array REST API service is unavailable for a period of time (typically around 30 minutes). This is expected behaviour. author: -- Everpure Ansible Team (@sdodsley) +- Everpure Ansible Team (@sdodsley) options: state: description: @@ -110,7 +110,7 @@ EXAMPLES = r""" - name: Check current ZTE status and retrieve sanitization certificate - purestorage.flasharray.purefa_zte: + everpure.flasharray.purefa_zte: state: status fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 @@ -123,14 +123,14 @@ when: zte.zte.sanitization_certificate | length > 0 - name: Start ZTE (Phase 1 - wipe drives) - purestorage.flasharray.purefa_zte: + everpure.flasharray.purefa_zte: state: start eradicate: true fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Start ZTE on a darksite array - purestorage.flasharray.purefa_zte: + everpure.flasharray.purefa_zte: state: start eradicate: true skip_phonehome_check: true @@ -138,7 +138,7 @@ api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Finalize ZTE without reinstalling the image (Option 1) - purestorage.flasharray.purefa_zte: + everpure.flasharray.purefa_zte: state: finalize eradicate: true reinstall_image: false @@ -146,7 +146,7 @@ api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Finalize ZTE and reinstall image on a phoning-home array (Option 2) - purestorage.flasharray.purefa_zte: + everpure.flasharray.purefa_zte: state: finalize eradicate: true reinstall_image: true @@ -155,7 +155,7 @@ api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Finalize ZTE and reinstall image on a darksite array (Option 2) - purestorage.flasharray.purefa_zte: + everpure.flasharray.purefa_zte: state: finalize eradicate: true reinstall_image: true @@ -165,7 +165,7 @@ api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Cancel a failed ZTE process - purestorage.flasharray.purefa_zte: + everpure.flasharray.purefa_zte: state: cancel fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 @@ -210,14 +210,14 @@ HAS_PURESTORAGE = False from ansible.module_utils.basic import AnsibleModule -from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( +from ansible_collections.everpure.flasharray.plugins.module_utils.purefa import ( get_array, purefa_argument_spec, ) -from ansible_collections.purestorage.flasharray.plugins.module_utils.version import ( +from ansible_collections.everpure.flasharray.plugins.module_utils.version import ( LooseVersion, ) -from ansible_collections.purestorage.flasharray.plugins.module_utils.api_helpers import ( +from ansible_collections.everpure.flasharray.plugins.module_utils.api_helpers import ( check_response, ) From e65b2befd716b0536ba1630682667b2850cd1a68 Mon Sep 17 00:00:00 2001 From: Simon Dodsley Date: Fri, 17 Jul 2026 11:48:24 -0400 Subject: [PATCH 3/3] Change module extension from purestorage to everpure --- plugins/modules/purefa_zte.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/purefa_zte.py b/plugins/modules/purefa_zte.py index d7e5a927..3cfc7a5b 100644 --- a/plugins/modules/purefa_zte.py +++ b/plugins/modules/purefa_zte.py @@ -105,7 +105,7 @@ darksite array. type: str extends_documentation_fragment: -- purestorage.flasharray.purestorage.fa +- everpure.flasharray.everpure.fa """ EXAMPLES = r"""