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