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