1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2020, 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: purefa_pgsched 20short_description: Manage protection groups replication schedules on Pure Storage FlashArrays 21version_added: '1.0.0' 22description: 23- Modify or delete protection groups replication schedules on Pure Storage FlashArrays. 24author: 25- Pure Storage Ansible Team (@sdodsley) <pure-ansible-team@purestorage.com> 26options: 27 name: 28 description: 29 - The name of the protection group. 30 type: str 31 required: true 32 state: 33 description: 34 - Define whether to set or delete the protection group schedule. 35 type: str 36 default: present 37 choices: [ absent, present ] 38 schedule: 39 description: 40 - Which schedule to change. 41 type: str 42 choices: ['replication', 'snapshot'] 43 required: True 44 enabled: 45 description: 46 - Enable the schedule being configured. 47 type: bool 48 default: True 49 replicate_at: 50 description: 51 - Specifies the preferred time as HH:MM:SS, using 24-hour clock, at which to generate snapshots. 52 type: int 53 blackout_start: 54 description: 55 - Specifies the time at which to suspend replication. 56 - Provide a time in 12-hour AM/PM format, eg. 11AM 57 type: str 58 blackout_end: 59 description: 60 - Specifies the time at which to restart replication. 61 - Provide a time in 12-hour AM/PM format, eg. 5PM 62 type: str 63 replicate_frequency: 64 description: 65 - Specifies the replication frequency in seconds. 66 - Range 900 - 34560000 (FA-405, //M10, //X10i and Cloud Block Store). 67 - Range 300 - 34560000 (all other arrays). 68 type: int 69 snap_at: 70 description: 71 - Specifies the preferred time as HH:MM:SS, using 24-hour clock, at which to generate snapshots. 72 - Only valid if I(snap_frequency) is an exact multiple of 86400, ie 1 day. 73 type: int 74 snap_frequency: 75 description: 76 - Specifies the snapshot frequency in seconds. 77 - Range available 300 - 34560000. 78 type: int 79 days: 80 description: 81 - Specifies the number of days to keep the I(per_day) snapshots beyond the 82 I(all_for) period before they are eradicated 83 - Max retention period is 4000 days 84 type: int 85 all_for: 86 description: 87 - Specifies the length of time, in seconds, to keep the snapshots on the 88 source array before they are eradicated. 89 - Range available 1 - 34560000. 90 type: int 91 per_day: 92 description: 93 - Specifies the number of I(per_day) snapshots to keep beyond the I(all_for) period. 94 - Maximum number is 1440 95 type: int 96 target_all_for: 97 description: 98 - Specifies the length of time, in seconds, to keep the replicated snapshots on the targets. 99 - Range is 1 - 34560000 seconds. 100 type: int 101 target_per_day: 102 description: 103 - Specifies the number of I(per_day) replicated snapshots to keep beyond the I(target_all_for) period. 104 - Maximum number is 1440 105 type: int 106 target_days: 107 description: 108 - Specifies the number of days to keep the I(target_per_day) replicated snapshots 109 beyond the I(target_all_for) period before they are eradicated. 110 - Max retention period is 4000 days 111 type: int 112extends_documentation_fragment: 113- purestorage.flasharray.purestorage.fa 114""" 115 116EXAMPLES = r""" 117- name: Update protection group snapshot schedule 118 purefa_pgsched: 119 name: foo 120 schedule: snapshot 121 enabled: true 122 snap_frequency: 86400 123 snap_at: 15:30:00 124 per_day: 5 125 all_for: 5 126 fa_url: 10.10.10.2 127 api_token: e31060a7-21fc-e277-6240-25983c6c4592 128 129- name: Update protection group replication schedule 130 purefa_pgsched: 131 name: foo 132 schedule: replication 133 enabled: true 134 replicate_frequency: 86400 135 replicate_at: 15:30:00 136 target_per_day: 5 137 target_all_for: 5 138 blackout_start: 2AM 139 blackout_end: 5AM 140 fa_url: 10.10.10.2 141 api_token: e31060a7-21fc-e277-6240-25983c6c4592 142 143- name: Delete protection group snapshot schedule 144 purefa_pgsched: 145 name: foo 146 scheduke: snapshot 147 state: absent 148 fa_url: 10.10.10.2 149 api_token: e31060a7-21fc-e277-6240-25983c6c4592 150 151- name: Delete protection group replication schedule 152 purefa_pgsched: 153 name: foo 154 scheduke: replication 155 state: absent 156 fa_url: 10.10.10.2 157 api_token: e31060a7-21fc-e277-6240-25983c6c4592 158""" 159 160RETURN = r""" 161""" 162 163from ansible.module_utils.basic import AnsibleModule 164from ansible_collections.purestorage.flasharray.plugins.module_utils.purefa import ( 165 get_system, 166 purefa_argument_spec, 167) 168 169 170def get_pending_pgroup(module, array): 171 """Get Protection Group""" 172 pgroup = None 173 if ":" in module.params["name"]: 174 for pgrp in array.list_pgroups(pending=True, on="*"): 175 if pgrp["name"] == module.params["name"] and pgrp["time_remaining"]: 176 pgroup = pgrp 177 break 178 else: 179 for pgrp in array.list_pgroups(pending=True): 180 if pgrp["name"] == module.params["name"] and pgrp["time_remaining"]: 181 pgroup = pgrp 182 break 183 184 return pgroup 185 186 187def get_pgroup(module, array): 188 """Get Protection Group""" 189 pgroup = None 190 if ":" in module.params["name"]: 191 for pgrp in array.list_pgroups(on="*"): 192 if pgrp["name"] == module.params["name"]: 193 pgroup = pgrp 194 break 195 else: 196 for pgrp in array.list_pgroups(): 197 if pgrp["name"] == module.params["name"]: 198 pgroup = pgrp 199 break 200 201 return pgroup 202 203 204def _convert_to_minutes(hour): 205 if hour[-2:] == "AM" and hour[:2] == "12": 206 return 0 207 elif hour[-2:] == "AM": 208 return int(hour[:-2]) * 3600 209 elif hour[-2:] == "PM" and hour[:2] == "12": 210 return 43200 211 return (int(hour[:-2]) + 12) * 3600 212 213 214def update_schedule(module, array): 215 """Update Protection Group Schedule""" 216 changed = False 217 try: 218 schedule = array.get_pgroup(module.params["name"], schedule=True) 219 retention = array.get_pgroup(module.params["name"], retention=True) 220 if not schedule["replicate_blackout"]: 221 schedule["replicate_blackout"] = [{"start": 0, "end": 0}] 222 except Exception: 223 module.fail_json( 224 msg="Failed to get current schedule for pgroup {0}.".format( 225 module.params["name"] 226 ) 227 ) 228 current_repl = { 229 "replicate_frequency": schedule["replicate_frequency"], 230 "replicate_enabled": schedule["replicate_enabled"], 231 "target_days": retention["target_days"], 232 "replicate_at": schedule["replicate_at"], 233 "target_per_day": retention["target_per_day"], 234 "target_all_for": retention["target_all_for"], 235 "blackout_start": schedule["replicate_blackout"][0]["start"], 236 "blackout_end": schedule["replicate_blackout"][0]["end"], 237 } 238 current_snap = { 239 "days": retention["days"], 240 "snap_frequency": schedule["snap_frequency"], 241 "snap_enabled": schedule["snap_enabled"], 242 "snap_at": schedule["snap_at"], 243 "per_day": retention["per_day"], 244 "all_for": retention["all_for"], 245 } 246 if module.params["schedule"] == "snapshot": 247 if not module.params["snap_frequency"]: 248 snap_frequency = current_snap["snap_frequency"] 249 else: 250 if not 300 <= module.params["snap_frequency"] <= 34560000: 251 module.fail_json( 252 msg="Snap Frequency support is out of range (300 to 34560000)" 253 ) 254 else: 255 snap_frequency = module.params["snap_frequency"] 256 257 if not module.params["snap_at"]: 258 snap_at = current_snap["snap_at"] 259 else: 260 snap_at = module.params["snap_at"] 261 262 if not module.params["days"]: 263 days = current_snap["days"] 264 else: 265 if module.params["days"] > 4000: 266 module.fail_json(msg="Maximum value for days is 4000") 267 else: 268 days = module.params["days"] 269 270 if not module.params["per_day"]: 271 per_day = current_snap["per_day"] 272 else: 273 if module.params["per_day"] > 1440: 274 module.fail_json(msg="Maximum value for per_day is 1440") 275 else: 276 per_day = module.params["per_day"] 277 278 if not module.params["all_for"]: 279 all_for = current_snap["all_for"] 280 else: 281 if module.params["all_for"] > 34560000: 282 module.fail_json(msg="Maximum all_for value is 34560000") 283 else: 284 all_for = module.params["all_for"] 285 new_snap = { 286 "days": days, 287 "snap_frequency": snap_frequency, 288 "snap_enabled": module.params["enabled"], 289 "snap_at": snap_at, 290 "per_day": per_day, 291 "all_for": all_for, 292 } 293 if current_snap != new_snap: 294 changed = True 295 if not module.check_mode: 296 try: 297 array.set_pgroup( 298 module.params["name"], snap_enabled=module.params["enabled"] 299 ) 300 array.set_pgroup( 301 module.params["name"], 302 snap_frequency=snap_frequency, 303 snap_at=snap_at, 304 ) 305 array.set_pgroup( 306 module.params["name"], 307 days=days, 308 per_day=per_day, 309 all_for=all_for, 310 ) 311 except Exception: 312 module.fail_json( 313 msg="Failed to change snapshot schedule for pgroup {0}.".format( 314 module.params["name"] 315 ) 316 ) 317 else: 318 if not module.params["replicate_frequency"]: 319 replicate_frequency = current_repl["replicate_frequency"] 320 else: 321 model = array.get(controllers=True)[0]["model"] 322 if "405" in model or "10" in model or "CBS" in model: 323 if not 900 <= module.params["replicate_frequency"] <= 34560000: 324 module.fail_json( 325 msg="Replication Frequency support is out of range (900 to 34560000)" 326 ) 327 else: 328 replicate_frequency = module.params["replicate_frequency"] 329 else: 330 if not 300 <= module.params["replicate_frequency"] <= 34560000: 331 module.fail_json( 332 msg="Replication Frequency support is out of range (300 to 34560000)" 333 ) 334 else: 335 replicate_frequency = module.params["replicate_frequency"] 336 337 if not module.params["replicate_at"]: 338 replicate_at = current_repl["replicate_at"] 339 else: 340 replicate_at = module.params["replicate_at"] 341 342 if not module.params["target_days"]: 343 target_days = current_repl["target_days"] 344 else: 345 if module.params["target_days"] > 4000: 346 module.fail_json(msg="Maximum value for target_days is 4000") 347 else: 348 target_days = module.params["target_days"] 349 350 if not module.params["target_per_day"]: 351 target_per_day = current_repl["target_per_day"] 352 else: 353 if module.params["target_per_day"] > 1440: 354 module.fail_json(msg="Maximum value for target_per_day is 1440") 355 else: 356 target_per_day = module.params["target_per_day"] 357 358 if not module.params["target_all_for"]: 359 target_all_for = current_repl["target_all_for"] 360 else: 361 if module.params["target_all_for"] > 34560000: 362 module.fail_json(msg="Maximum target_all_for value is 34560000") 363 else: 364 target_all_for = module.params["target_all_for"] 365 if not module.params["blackout_end"]: 366 blackout_end = current_repl["blackout_start"] 367 else: 368 blackout_end = _convert_to_minutes(module.params["blackout_end"]) 369 if not module.params["blackout_start"]: 370 blackout_start = current_repl["blackout_start"] 371 else: 372 blackout_start = _convert_to_minutes(module.params["blackout_start"]) 373 374 new_repl = { 375 "replicate_frequency": replicate_frequency, 376 "replicate_enabled": module.params["enabled"], 377 "target_days": target_days, 378 "replicate_at": replicate_at, 379 "target_per_day": target_per_day, 380 "target_all_for": target_all_for, 381 "blackout_start": blackout_start, 382 "blackout_end": blackout_end, 383 } 384 if current_repl != new_repl: 385 changed = True 386 if not module.check_mode: 387 blackout = {"start": blackout_start, "end": blackout_end} 388 try: 389 array.set_pgroup( 390 module.params["name"], 391 replicate_enabled=module.params["enabled"], 392 ) 393 array.set_pgroup( 394 module.params["name"], 395 replicate_frequency=replicate_frequency, 396 replicate_at=replicate_at, 397 ) 398 if blackout_start == 0: 399 array.set_pgroup(module.params["name"], replicate_blackout=None) 400 else: 401 array.set_pgroup( 402 module.params["name"], replicate_blackout=blackout 403 ) 404 array.set_pgroup( 405 module.params["name"], 406 target_days=target_days, 407 target_per_day=target_per_day, 408 target_all_for=target_all_for, 409 ) 410 except Exception: 411 module.fail_json( 412 msg="Failed to change replication schedule for pgroup {0}.".format( 413 module.params["name"] 414 ) 415 ) 416 417 module.exit_json(changed=changed) 418 419 420def delete_schedule(module, array): 421 """Delete, ie. disable, Protection Group Schedules""" 422 changed = False 423 try: 424 current_state = array.get_pgroup(module.params["name"], schedule=True) 425 if module.params["schedule"] == "replication": 426 if current_state["replicate_enabled"]: 427 changed = True 428 if not module.check_mode: 429 array.set_pgroup(module.params["name"], replicate_enabled=False) 430 array.set_pgroup( 431 module.params["name"], 432 target_days=0, 433 target_per_day=0, 434 target_all_for=1, 435 ) 436 array.set_pgroup( 437 module.params["name"], 438 replicate_frequency=14400, 439 replicate_blackout=None, 440 ) 441 else: 442 if current_state["snap_enabled"]: 443 changed = True 444 if not module.check_mode: 445 array.set_pgroup(module.params["name"], snap_enabled=False) 446 array.set_pgroup( 447 module.params["name"], days=0, per_day=0, all_for=1 448 ) 449 array.set_pgroup(module.params["name"], snap_frequency=300) 450 except Exception: 451 module.fail_json( 452 msg="Deleting pgroup {0} {1} schedule failed.".format( 453 module.params["name"], module.params["schedule"] 454 ) 455 ) 456 module.exit_json(changed=changed) 457 458 459def main(): 460 argument_spec = purefa_argument_spec() 461 argument_spec.update( 462 dict( 463 name=dict(type="str", required=True), 464 state=dict(type="str", default="present", choices=["absent", "present"]), 465 schedule=dict( 466 type="str", required=True, choices=["replication", "snapshot"] 467 ), 468 blackout_start=dict(type="str"), 469 blackout_end=dict(type="str"), 470 snap_at=dict(type="int"), 471 replicate_at=dict(type="int"), 472 replicate_frequency=dict(type="int"), 473 snap_frequency=dict(type="int"), 474 all_for=dict(type="int"), 475 days=dict(type="int"), 476 per_day=dict(type="int"), 477 target_all_for=dict(type="int"), 478 target_per_day=dict(type="int"), 479 target_days=dict(type="int"), 480 enabled=dict(type="bool", default=True), 481 ) 482 ) 483 484 required_together = [["blackout_start", "blackout_end"]] 485 486 module = AnsibleModule( 487 argument_spec, required_together=required_together, supports_check_mode=True 488 ) 489 490 state = module.params["state"] 491 array = get_system(module) 492 493 pgroup = get_pgroup(module, array) 494 if module.params["snap_at"] and module.params["snap_frequency"]: 495 if not module.params["snap_frequency"] % 86400 == 0: 496 module.fail_json( 497 msg="snap_at not valid unless snapshot frequency is measured in days, ie. a multiple of 86400" 498 ) 499 if pgroup and state == "present": 500 update_schedule(module, array) 501 elif pgroup and state == "absent": 502 delete_schedule(module, array) 503 elif pgroup is None: 504 module.fail_json( 505 msg="Specified protection group {0} does not exist.".format( 506 module.params["pgroup"] 507 ) 508 ) 509 510 511if __name__ == "__main__": 512 main() 513