1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# (c) 2017, Simon Dodsley (simon@purestorage.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_snap
20version_added: '1.0.0'
21short_description: Manage filesystem snapshots on Pure Storage FlashBlades
22description:
23- Create or delete volumes and filesystem snapshots on Pure Storage FlashBlades.
24- Restoring a filesystem from a snapshot is only supported using
25  the latest snapshot.
26author:
27- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com>
28options:
29  name:
30    description:
31    - The name of the source filesystem.
32    required: true
33    type: str
34  suffix:
35    description:
36    - Suffix of snapshot name.
37    type: str
38  state:
39    description:
40    - Define whether the filesystem snapshot should exist or not.
41    choices: [ absent, present, restore ]
42    default: present
43    type: str
44  targets:
45    description:
46    - Name of target to replicate snapshot to.
47    - This is only applicable when I(now) is B(True)
48    type: list
49    elements: str
50    version_added: "1.7.0"
51  now:
52    description:
53    - Whether to initiate a snapshot replication immeadiately
54    type: bool
55    default: False
56    version_added: "1.7.0"
57  eradicate:
58    description:
59    - Define whether to eradicate the snapshot on delete or leave in trash.
60    type: bool
61    default: 'no'
62extends_documentation_fragment:
63- purestorage.flashblade.purestorage.fb
64"""
65
66EXAMPLES = r"""
67- name: Create snapshot foo.ansible
68  purefb_snap:
69    name: foo
70    suffix: ansible
71    fb_url: 10.10.10.2
72    api_token: e31060a7-21fc-e277-6240-25983c6c4592
73    state: present
74
75- name: Create immeadiate snapshot foo.ansible to connected FB bar
76  purefb_snap:
77    name: foo
78    suffix: ansible
79    now: True
80    targets:
81    - bar
82    fb_url: 10.10.10.2
83    api_token: e31060a7-21fc-e277-6240-25983c6c4592
84    state: present
85
86- name: Delete snapshot named foo.snap
87  purefb_snap:
88    name: foo
89    suffix: snap
90    fb_url: 10.10.10.2
91    api_token: e31060a7-21fc-e277-6240-25983c6c4592
92    state: absent
93
94- name: Recover deleted snapshot foo.ansible
95  purefb_snap:
96    name: foo
97    suffix: ansible
98    fb_url: 10.10.10.2
99    api_token: e31060a7-21fc-e277-6240-25983c6c4592
100    state: present
101
102- name: Restore filesystem foo (uses latest snapshot)
103  purefb_snap:
104    name: foo
105    fb_url: 10.10.10.2
106    api_token: e31060a7-21fc-e277-6240-25983c6c4592
107    state: restore
108
109- name: Eradicate snapshot named foo.snap
110  purefb_snap:
111    name: foo
112    suffix: snap
113    eradicate: true
114    fb_url: 10.10.10.2
115    api_token: e31060a7-21fc-e277-6240-25983c6c4592
116    state: absent
117"""
118
119RETURN = r"""
120"""
121
122from ansible.module_utils.basic import AnsibleModule
123from ansible_collections.purestorage.flashblade.plugins.module_utils.purefb import (
124    get_blade,
125    purefb_argument_spec,
126)
127
128from datetime import datetime
129
130HAS_PURITY_FB = True
131try:
132    from purity_fb import FileSystemSnapshot, SnapshotSuffix, FileSystem, Reference
133except ImportError:
134    HAS_PURITY_FB = False
135
136SNAP_NOW_API = 1.10
137
138
139def get_fs(module, blade):
140    """Return Filesystem or None"""
141    filesystem = []
142    filesystem.append(module.params["name"])
143    try:
144        res = blade.file_systems.list_file_systems(names=filesystem)
145        return res.items[0]
146    except Exception:
147        return None
148
149
150def get_latest_fssnapshot(module, blade):
151    """Get the name of the latest snpshot or None"""
152    try:
153        filt = "source='" + module.params["name"] + "'"
154        all_snaps = blade.file_system_snapshots.list_file_system_snapshots(filter=filt)
155        if not all_snaps.items[0].destroyed:
156            return all_snaps.items[0].name
157        else:
158            module.fail_json(
159                msg="Latest snapshot {0} is destroyed."
160                " Eradicate or recover this first.".format(all_snaps.items[0].name)
161            )
162    except Exception:
163        return None
164
165
166def get_fssnapshot(module, blade):
167    """Return Snapshot or None"""
168    try:
169        filt = (
170            "source='"
171            + module.params["name"]
172            + "' and suffix='"
173            + module.params["suffix"]
174            + "'"
175        )
176        res = blade.file_system_snapshots.list_file_system_snapshots(filter=filt)
177        return res.items[0]
178    except Exception:
179        return None
180
181
182def create_snapshot(module, blade):
183    """Create Snapshot"""
184    changed = False
185    source = []
186    source.append(module.params["name"])
187    try:
188        if module.params["now"]:
189            blade_exists = []
190            connected_blades = blade.array_connections.list_array_connections()
191            for target in range(0, len(module.params["targets"])):
192                blade_exists.append(False)
193                for blade in range(0, len(connected_blades)):
194                    if (
195                        target[target] == connected_blades.items[blade].name
196                        and connected_blades.items[blade].status == "connected"
197                    ):
198                        blade_exists[target] = True
199            if not blade_exists:
200                module.fail_json(
201                    msg="Not all selected targets are correctly connected blades"
202                )
203            changed = True
204            if not module.check_mode:
205                blade.file_system_snapshots.create_file_system_snapshots(
206                    sources=source,
207                    send=True,
208                    targets=module.params["targets"],
209                    suffix=SnapshotSuffix(module.params["suffix"]),
210                )
211        else:
212            changed = True
213            if not module.check_mode:
214                blade.file_system_snapshots.create_file_system_snapshots(
215                    sources=source, suffix=SnapshotSuffix(module.params["suffix"])
216                )
217    except Exception:
218        changed = False
219    module.exit_json(changed=changed)
220
221
222def restore_snapshot(module, blade):
223    """Restore a filesystem back from the latest snapshot"""
224    changed = True
225    snapname = get_latest_fssnapshot(module, blade)
226    if snapname is not None:
227        if not module.check_mode:
228            fs_attr = FileSystem(
229                name=module.params["name"], source=Reference(name=snapname)
230            )
231            try:
232                blade.file_systems.create_file_systems(
233                    overwrite=True,
234                    discard_non_snapshotted_data=True,
235                    file_system=fs_attr,
236                )
237            except Exception:
238                changed = False
239    else:
240        module.fail_json(
241            msg="Filesystem {0} has no snapshots to restore from.".format(
242                module.params["name"]
243            )
244        )
245    module.exit_json(changed=changed)
246
247
248def recover_snapshot(module, blade):
249    """Recover deleted Snapshot"""
250    changed = True
251    if not module.check_mode:
252        snapname = module.params["name"] + "." + module.params["suffix"]
253        new_attr = FileSystemSnapshot(destroyed=False)
254        try:
255            blade.file_system_snapshots.update_file_system_snapshots(
256                name=snapname, attributes=new_attr
257            )
258        except Exception:
259            changed = False
260    module.exit_json(changed=changed)
261
262
263def update_snapshot(module, blade):
264    """Update Snapshot"""
265    changed = False
266    module.exit_json(changed=changed)
267
268
269def delete_snapshot(module, blade):
270    """Delete Snapshot"""
271    if not module.check_mode:
272        snapname = module.params["name"] + "." + module.params["suffix"]
273        new_attr = FileSystemSnapshot(destroyed=True)
274        try:
275            blade.file_system_snapshots.update_file_system_snapshots(
276                name=snapname, attributes=new_attr
277            )
278            changed = True
279            if module.params["eradicate"]:
280                try:
281                    blade.file_system_snapshots.delete_file_system_snapshots(
282                        name=snapname
283                    )
284                    changed = True
285                except Exception:
286                    changed = False
287        except Exception:
288            changed = False
289    module.exit_json(changed=changed)
290
291
292def eradicate_snapshot(module, blade):
293    """Eradicate Snapshot"""
294    if not module.check_mode:
295        snapname = module.params["name"] + "." + module.params["suffix"]
296        try:
297            blade.file_system_snapshots.delete_file_system_snapshots(name=snapname)
298            changed = True
299        except Exception:
300            changed = False
301    module.exit_json(changed=changed)
302
303
304def main():
305    argument_spec = purefb_argument_spec()
306    argument_spec.update(
307        dict(
308            name=dict(required=True),
309            suffix=dict(type="str"),
310            now=dict(type="bool", default=False),
311            targets=dict(type="list", elements="str"),
312            eradicate=dict(default="false", type="bool"),
313            state=dict(default="present", choices=["present", "absent", "restore"]),
314        )
315    )
316
317    required_if = [["now", True, ["targets"]]]
318    module = AnsibleModule(
319        argument_spec, required_if=required_if, supports_check_mode=True
320    )
321
322    if not HAS_PURITY_FB:
323        module.fail_json(msg="purity_fb sdk is required for this module")
324
325    if module.params["suffix"] is None:
326        suffix = "snap-" + str(
327            (datetime.utcnow() - datetime(1970, 1, 1, 0, 0, 0, 0)).total_seconds()
328        )
329        module.params["suffix"] = suffix.replace(".", "")
330
331    state = module.params["state"]
332    blade = get_blade(module)
333    versions = blade.api_version.list_versions().versions
334
335    if SNAP_NOW_API not in versions and module.params["now"]:
336        module.fail_json(
337            msg="Minimum FlashBlade REST version for immeadiate remote snapshots: {0}".format(
338                SNAP_NOW_API
339            )
340        )
341    filesystem = get_fs(module, blade)
342    snap = get_fssnapshot(module, blade)
343
344    if state == "present" and filesystem and not filesystem.destroyed and not snap:
345        create_snapshot(module, blade)
346    elif (
347        state == "present"
348        and filesystem
349        and not filesystem.destroyed
350        and snap
351        and not snap.destroyed
352    ):
353        update_snapshot(module, blade)
354    elif (
355        state == "present"
356        and filesystem
357        and not filesystem.destroyed
358        and snap
359        and snap.destroyed
360    ):
361        recover_snapshot(module, blade)
362    elif state == "present" and filesystem and filesystem.destroyed:
363        update_snapshot(module, blade)
364    elif state == "present" and not filesystem:
365        update_snapshot(module, blade)
366    elif state == "restore" and filesystem:
367        restore_snapshot(module, blade)
368    elif state == "absent" and snap and not snap.destroyed:
369        delete_snapshot(module, blade)
370    elif state == "absent" and snap and snap.destroyed:
371        eradicate_snapshot(module, blade)
372    elif state == "absent" and not snap:
373        module.exit_json(changed=False)
374    else:
375        module.exit_json(changed=False)
376
377
378if __name__ == "__main__":
379    main()
380