#!/usr/bin/python # -*- coding: utf-8 -*- # (c) 2021, 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_dirsnap version_added: '1.9.0' short_description: Manage FlashArray File System Directory Snapshots description: - Create/Delete FlashArray File System directory snapshots - A full snapshot name is constructed in the form of DIR.CLIENT_NAME.SUFFIX where DIR is the managed directory name, CLIENT_NAME is the client name, and SUFFIX is the suffix. - The client visible snapshot name is CLIENT_NAME.SUFFIX. author: - Pure Storage Ansible Team (@sdodsley) options: name: description: - Name of the directory to snapshot type: str required: true state: description: - Define whether the directory snapshot should exist or not. default: present choices: [ absent, present ] type: str filesystem: description: - Name of the filesystem the directory links to. type: str required: true eradicate: description: - Define whether to eradicate the snapshot on delete or leave in trash type: bool default: false client: description: - The client name portion of the client visible snapshot name type: str required: true suffix: description: - Snapshot suffix to use type: str new_client: description: - The new client name when performing a rename type: str version_added: '1.12.0' new_suffix: description: - The new suffix when performing a rename type: str version_added: '1.12.0' rename: description: - Whether to rename a directory snapshot - The snapshot client name and suffix can be changed - Required with I(new_client) ans I(new_suffix) type: bool default: false version_added: '1.12.0' keep_for: description: - Retention period, after which snapshots will be eradicated - Specify in seconds. Range 300 - 31536000 (5 minutes to 1 year) - Value of 0 will set no retention period. - If not specified on create will default to 0 (no retention period) type: int extends_documentation_fragment: - purestorage.flasharray.purestorage.fa """ EXAMPLES = r""" - name: Create a snapshot direcotry foo in filesysten bar for client test with suffix test purestorage.flasharray.purefa_dirsnap: name: foo filesystem: bar client: test suffix: test fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Update retention time for a snapshot foo:bar.client.test purestorage.flasharray.purefa_dirsnap: name: foo filesystem: bar client: client suffix: test keep_for: 300 # 5 minutes fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Delete snapshot foo:bar.client.test purestorage.flasharray.purefa_dirsnap: name: foo filesystem: bar client: client suffix: test state: absent fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Recover deleted snapshot foo:bar.client.test purestorage.flasharray.purefa_dirsnap: name: foo filesystem: bar client: client suffix: test fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Delete and eradicate snapshot foo:bar.client.test purestorage.flasharray.purefa_dirsnap: name: foo filesystem: bar client: client suffix: test state: absent eradicate: true fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Eradicate deleted snapshot foo:bar.client.test purestorage.flasharray.purefa_dirsnap: name: foo filesystem: bar client: client suffix: test eradicate: true state: absent fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 - name: Rename snapshot purestorage.flasharray.purefa_dirsnap: name: foo filesystem: bar client: client suffix: test rename: true new_client: client2 new_suffix: test2 fa_url: 10.10.10.2 api_token: e31060a7-21fc-e277-6240-25983c6c4592 """ RETURN = r""" """ HAS_PURESTORAGE = True try: from pypureclient.flasharray import DirectorySnapshotPost, DirectorySnapshotPatch except ImportError: HAS_PURESTORAGE = False import re 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, ) MIN_REQUIRED_API_VERSION = "2.2" MIN_RENAME_API_VERSION = "2.10" def eradicate_snap(module, array): """Eradicate a filesystem snapshot""" changed = True if not module.check_mode: snapname = ( module.params["filesystem"] + ":" + module.params["name"] + "." + module.params["client"] + "." + module.params["suffix"] ) res = array.delete_directory_snapshots(names=[snapname]) if res.status_code != 200: module.fail_json( msg="Failed to eradicate filesystem snapshot {0}. Error: {1}".format( snapname, res.errors[0].message ) ) module.exit_json(changed=changed) def delete_snap(module, array): """Delete a filesystem snapshot""" changed = True if not module.check_mode: snapname = ( module.params["filesystem"] + ":" + module.params["name"] + "." + module.params["client"] + "." + module.params["suffix"] ) directory_snapshot = DirectorySnapshotPatch(destroyed=True) res = array.patch_directory_snapshots( names=[snapname], directory_snapshot=directory_snapshot ) if res.status_code != 200: module.fail_json( msg="Failed to delete filesystem snapshot {0}. Error: {1}".format( snapname, res.errors[0].message ) ) if module.params["eradicate"]: eradicate_snap(module, array) module.exit_json(changed=changed) def update_snap(module, array, snap_detail): """Update a filesystem snapshot retention time""" changed = True snapname = ( module.params["filesystem"] + ":" + module.params["name"] + "." + module.params["client"] + "." + module.params["suffix"] ) if module.params["rename"]: if not module.params["new_client"]: new_client = module.params["client"] else: new_client = module.params["new_client"] if not module.params["new_suffix"]: new_suffix = module.params["suffix"] else: new_suffix = module.params["new_suffix"] new_snapname = ( module.params["filesystem"] + ":" + module.params["name"] + "." + new_client + "." + new_suffix ) directory_snapshot = DirectorySnapshotPatch( client_name=new_client, suffix=new_suffix ) if not module.check_mode: res = array.patch_directory_snapshots( names=[snapname], directory_snapshot=directory_snapshot ) if res.status_code != 200: module.fail_json( msg="Failed to rename snapshot {0}. Error: {1}".format( snapname, res.errors[0].message ) ) else: snapname = new_snapname if not module.params["keep_for"] or module.params["keep_for"] == 0: keep_for = 0 elif 300 <= module.params["keep_for"] <= 31536000: keep_for = module.params["keep_for"] * 1000 else: module.fail_json(msg="keep_for not in range of 300 - 31536000") if not module.check_mode: if snap_detail.destroyed: directory_snapshot = DirectorySnapshotPatch(destroyed=False) res = array.patch_directory_snapshots( names=[snapname], directory_snapshot=directory_snapshot ) if res.status_code != 200: module.fail_json( msg="Failed to recover snapshot {0}. Error: {1}".format( snapname, res.errors[0].message ) ) directory_snapshot = DirectorySnapshotPatch(keep_for=keep_for) if snap_detail.time_remaining == 0 and keep_for != 0: res = array.patch_directory_snapshots( names=[snapname], directory_snapshot=directory_snapshot ) if res.status_code != 200: module.fail_json( msg="Failed to retention time for snapshot {0}. Error: {1}".format( snapname, res.errors[0].message ) ) elif snap_detail.time_remaining > 0: if module.params["rename"] and module.params["keep_for"]: res = array.patch_directory_snapshots( names=[snapname], directory_snapshot=directory_snapshot ) if res.status_code != 200: module.fail_json( msg="Failed to retention time for renamed snapshot {0}. Error: {1}".format( snapname, res.errors[0].message ) ) module.exit_json(changed=changed) def create_snap(module, array): """Create a filesystem snapshot""" changed = True if not module.check_mode: if not module.params["keep_for"] or module.params["keep_for"] == 0: keep_for = 0 elif 300 <= module.params["keep_for"] <= 31536000: keep_for = module.params["keep_for"] * 1000 else: module.fail_json(msg="keep_for not in range of 300 - 31536000") directory = module.params["filesystem"] + ":" + module.params["name"] if module.params["suffix"]: directory_snapshot = DirectorySnapshotPost( client_name=module.params["client"], keep_for=keep_for, suffix=module.params["suffix"], ) else: directory_snapshot = DirectorySnapshotPost( client_name=module.params["client"], keep_for=keep_for ) res = array.post_directory_snapshots( source_names=[directory], directory_snapshot=directory_snapshot ) if res.status_code != 200: module.fail_json( msg="Failed to create client {0} snapshot for {1}. Error: {2}".format( module.params["client"], directory, res.errors[0].message ) ) module.exit_json(changed=changed) def main(): argument_spec = purefa_argument_spec() argument_spec.update( dict( state=dict(type="str", default="present", choices=["absent", "present"]), filesystem=dict(type="str", required=True), name=dict(type="str", required=True), eradicate=dict(type="bool", default=False), client=dict(type="str", required=True), suffix=dict(type="str"), rename=dict(type="bool", default=False), new_client=dict(type="str"), new_suffix=dict(type="str"), keep_for=dict(type="int"), ) ) required_if = [["state", "absent", ["suffix"]]] module = AnsibleModule( argument_spec, required_if=required_if, supports_check_mode=True ) if module.params["rename"]: if not module.params["new_client"] and not module.params["new_suffix"]: module.fail_json(msg="Rename requires one of: new_client, new_suffix") if not HAS_PURESTORAGE: module.fail_json(msg="py-pure-client sdk is required for this module") client_pattern = re.compile( "^(?=.*[a-zA-Z-])[a-zA-Z0-9]([a-zA-Z0-9-]{0,56}[a-zA-Z0-9])?$" ) suffix_pattern = re.compile( "^(?=.*[a-zA-Z-])[a-zA-Z0-9]([a-zA-Z0-9-]{0,63}[a-zA-Z0-9])?$" ) if module.params["suffix"]: if not suffix_pattern.match(module.params["suffix"]): module.fail_json( msg="Suffix name {0} does not conform to the suffix name rules.".format( module.params["suffix"] ) ) if module.params["new_suffix"]: if not suffix_pattern.match(module.params["new_suffix"]): module.fail_json( msg="Suffix rename {0} does not conform to the suffix name rules.".format( module.params["new_suffix"] ) ) if module.params["client"]: if not client_pattern.match(module.params["client"]): module.fail_json( msg="Client name {0} does not conform to the client name rules.".format( module.params["client"] ) ) 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) ) if module.params["rename"] and LooseVersion(MIN_RENAME_API_VERSION) > LooseVersion( api_version ): module.fail_json( msg="Directory snapshot rename not supported. " "Minimum Purity//FA version required: 6.2.1" ) state = module.params["state"] snapshot_root = module.params["filesystem"] + ":" + module.params["name"] if bool( array.get_directories( filter='name="' + snapshot_root + '"', total_item_count=True ).total_item_count == 0 ): module.fail_json(msg="Directory {0} does not exist.".format(snapshot_root)) snap_exists = False if module.params["suffix"]: snap_detail = array.get_directory_snapshots( filter="name='" + snapshot_root + "." + module.params["client"] + "." + module.params["suffix"] + "'", total_item_count=True, ) if bool(snap_detail.status_code == 200): snap_exists = bool(snap_detail.total_item_count != 0) if snap_exists: snap_facts = list(snap_detail.items)[0] if state == "present" and not snap_exists: create_snap(module, array) elif state == "present" and snap_exists and module.params["suffix"]: update_snap(module, array, snap_facts) elif state == "absent" and snap_exists and not snap_facts.destroyed: delete_snap(module, array) elif ( state == "absent" and snap_exists and snap_facts.destroyed and module.params["eradicate"] ): eradicate_snap(module, array) else: module.exit_json(changed=False) module.exit_json(changed=False) if __name__ == "__main__": main()