1#!/usr/local/bin/python3.8
2# (c) 2017, NetApp, Inc
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5"""Element SW Software Snapshot Schedule"""
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['preview'],
13                    'supported_by': 'certified'}
14
15
16DOCUMENTATION = '''
17
18module: na_elementsw_snapshot_schedule
19
20short_description: NetApp Element Software Snapshot Schedules
21extends_documentation_fragment:
22    - netapp.elementsw.netapp.solidfire
23version_added: 2.7.0
24author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
25description:
26- Create, destroy, or update snapshot schedules on ElementSW
27
28options:
29
30    state:
31        description:
32        - Whether the specified schedule should exist or not.
33        choices: ['present', 'absent']
34        default: present
35        type: str
36
37    paused:
38        description:
39        - Pause / Resume a schedule.
40        type: bool
41
42    recurring:
43        description:
44        - Should the schedule recur?
45        type: bool
46
47    schedule_type:
48        description:
49        - Schedule type for creating schedule.
50        choices: ['DaysOfWeekFrequency','DaysOfMonthFrequency','TimeIntervalFrequency']
51        type: str
52
53    time_interval_days:
54        description: Time interval in days.
55        type: int
56
57    time_interval_hours:
58        description: Time interval in hours.
59        type: int
60
61    time_interval_minutes:
62        description: Time interval in minutes.
63        type: int
64
65    days_of_week_weekdays:
66        description: List of days of the week (Sunday to Saturday)
67        type: list
68        elements: str
69
70    days_of_week_hours:
71        description: Time specified in hours
72        type: int
73
74    days_of_week_minutes:
75        description:  Time specified in minutes.
76        type: int
77
78    days_of_month_monthdays:
79        description: List of days of the month (1-31)
80        type: list
81        elements: int
82
83    days_of_month_hours:
84        description: Time specified in hours
85        type: int
86
87    days_of_month_minutes:
88        description:  Time specified in minutes.
89        type: int
90
91    name:
92        description:
93        - Name for the snapshot schedule.
94        - It accepts either schedule_id or schedule_name
95        - if name is digit, it will consider as schedule_id
96        - If name is string, it will consider as schedule_name
97        required: true
98        type: str
99
100    snapshot_name:
101        description:
102        - Name for the created snapshots.
103        type: str
104
105    volumes:
106        description:
107        - Volume IDs that you want to set the snapshot schedule for.
108        - It accepts both volume_name and volume_id
109        type: list
110        elements: str
111
112    account_id:
113        description:
114        - Account ID for the owner of this volume.
115        - It accepts either account_name or account_id
116        - if account_id is digit, it will consider as account_id
117        - If account_id is string, it will consider as account_name
118        type: str
119
120    retention:
121        description:
122        - Retention period for the snapshot.
123        - Format is 'HH:mm:ss'.
124        type: str
125
126    starting_date:
127        description:
128        - Starting date for the schedule.
129        - Required when C(state=present).
130        - "Format: C(2016-12-01T00:00:00Z)"
131        type: str
132'''
133
134EXAMPLES = """
135   - name: Create Snapshot schedule
136     na_elementsw_snapshot_schedule:
137       hostname: "{{ elementsw_hostname }}"
138       username: "{{ elementsw_username }}"
139       password: "{{ elementsw_password }}"
140       state: present
141       name: Schedule_A
142       schedule_type: TimeIntervalFrequency
143       time_interval_days: 1
144       starting_date: '2016-12-01T00:00:00Z'
145       retention: '24:00:00'
146       volumes:
147       - 7
148       - test
149       account_id: 1
150
151   - name: Update Snapshot schedule
152     na_elementsw_snapshot_schedule:
153       hostname: "{{ elementsw_hostname }}"
154       username: "{{ elementsw_username }}"
155       password: "{{ elementsw_password }}"
156       state: present
157       name: Schedule_A
158       schedule_type: TimeIntervalFrequency
159       time_interval_days: 1
160       starting_date: '2016-12-01T00:00:00Z'
161       retention: '24:00:00'
162       volumes:
163       - 8
164       - test1
165       account_id: 1
166
167   - name: Delete Snapshot schedule
168     na_elementsw_snapshot_schedule:
169       hostname: "{{ elementsw_hostname }}"
170       username: "{{ elementsw_username }}"
171       password: "{{ elementsw_password }}"
172       state: absent
173       name: 6
174"""
175
176RETURN = """
177
178schedule_id:
179    description: Schedule ID of the newly created schedule
180    returned: success
181    type: str
182"""
183import traceback
184from ansible.module_utils.basic import AnsibleModule
185from ansible.module_utils._text import to_native
186import ansible_collections.netapp.elementsw.plugins.module_utils.netapp as netapp_utils
187from ansible_collections.netapp.elementsw.plugins.module_utils.netapp_elementsw_module import NaElementSWModule
188
189HAS_SF_SDK = netapp_utils.has_sf_sdk()
190try:
191    from solidfire.custom.models import DaysOfWeekFrequency, Weekday, DaysOfMonthFrequency
192    from solidfire.common import ApiConnectionError, ApiServerError
193    from solidfire.custom.models import TimeIntervalFrequency
194    from solidfire.models import Schedule, ScheduleInfo
195except ImportError:
196    HAS_SF_SDK = False
197
198try:
199    # Hack to see if we we have the 1.7 version of the SDK, or later
200    from solidfire.common.model import VER3
201    HAS_SF_SDK_1_7 = True
202    del VER3
203except ImportError:
204    HAS_SF_SDK_1_7 = False
205
206
207class ElementSWSnapShotSchedule(object):
208    """
209    Contains methods to parse arguments,
210    derive details of ElementSW objects
211    and send requests to ElementSW via
212    the ElementSW SDK
213    """
214
215    def __init__(self):
216        """
217        Parse arguments, setup state variables,
218        check paramenters and ensure SDK is installed
219        """
220        self.argument_spec = netapp_utils.ontap_sf_host_argument_spec()
221        self.argument_spec.update(dict(
222            state=dict(required=False, type='str', choices=['present', 'absent'], default='present'),
223            name=dict(required=True, type='str'),
224            schedule_type=dict(required=False, choices=['DaysOfWeekFrequency', 'DaysOfMonthFrequency', 'TimeIntervalFrequency']),
225
226            time_interval_days=dict(required=False, type='int'),
227            time_interval_hours=dict(required=False, type='int'),
228            time_interval_minutes=dict(required=False, type='int'),
229
230            days_of_week_weekdays=dict(required=False, type='list', elements='str'),
231            days_of_week_hours=dict(required=False, type='int'),
232            days_of_week_minutes=dict(required=False, type='int'),
233
234            days_of_month_monthdays=dict(required=False, type='list', elements='int'),
235            days_of_month_hours=dict(required=False, type='int'),
236            days_of_month_minutes=dict(required=False, type='int'),
237
238            paused=dict(required=False, type='bool'),
239            recurring=dict(required=False, type='bool'),
240
241            starting_date=dict(required=False, type='str'),
242
243            snapshot_name=dict(required=False, type='str'),
244            volumes=dict(required=False, type='list', elements='str'),
245            account_id=dict(required=False, type='str'),
246            retention=dict(required=False, type='str'),
247        ))
248
249        self.module = AnsibleModule(
250            argument_spec=self.argument_spec,
251            required_if=[
252                ('state', 'present', ['account_id', 'volumes', 'schedule_type']),
253                ('schedule_type', 'DaysOfMonthFrequency', ['days_of_month_monthdays']),
254                ('schedule_type', 'DaysOfWeekFrequency', ['days_of_week_weekdays'])
255
256            ],
257            supports_check_mode=True
258        )
259
260        param = self.module.params
261
262        # set up state variables
263        self.state = param['state']
264        self.name = param['name']
265        self.schedule_type = param['schedule_type']
266        self.days_of_week_weekdays = param['days_of_week_weekdays']
267        self.days_of_week_hours = param['days_of_week_hours']
268        self.days_of_week_minutes = param['days_of_week_minutes']
269        self.days_of_month_monthdays = param['days_of_month_monthdays']
270        self.days_of_month_hours = param['days_of_month_hours']
271        self.days_of_month_minutes = param['days_of_month_minutes']
272        self.time_interval_days = param['time_interval_days']
273        self.time_interval_hours = param['time_interval_hours']
274        self.time_interval_minutes = param['time_interval_minutes']
275        self.paused = param['paused']
276        self.recurring = param['recurring']
277        if self.schedule_type == 'DaysOfWeekFrequency':
278            # Create self.weekday list if self.schedule_type is days_of_week
279            if self.days_of_week_weekdays is not None:
280                # Create self.weekday list if self.schedule_type is days_of_week
281                self.weekdays = []
282                for day in self.days_of_week_weekdays:
283                    if str(day).isdigit():
284                        # If id specified, return appropriate day
285                        self.weekdays.append(Weekday.from_id(int(day)))
286                    else:
287                        # If name specified, return appropriate day
288                        self.weekdays.append(Weekday.from_name(day.capitalize()))
289
290        if self.state == 'present' and self.schedule_type is None:
291            # Mandate schedule_type for create operation
292            self.module.fail_json(
293                msg="Please provide required parameter: schedule_type")
294
295        # Mandate schedule name for delete operation
296        if self.state == 'absent' and self.name is None:
297            self.module.fail_json(
298                msg="Please provide required parameter: name")
299
300        self.starting_date = param['starting_date']
301        self.snapshot_name = param['snapshot_name']
302        self.volumes = param['volumes']
303        self.account_id = param['account_id']
304        self.retention = param['retention']
305        self.create_schedule_result = None
306
307        if HAS_SF_SDK is False:
308            # Create ElementSW connection
309            self.module.fail_json(msg="Unable to import the ElementSW Python SDK")
310        else:
311            self.sfe = netapp_utils.create_sf_connection(module=self.module)
312            self.elementsw_helper = NaElementSWModule(self.sfe)
313
314    def get_schedule(self):
315        # Checking whether schedule id is exist or not
316        # Return schedule details if found, None otherwise
317        # If exist set variable self.name
318        try:
319            schedule_list = self.sfe.list_schedules()
320        except ApiServerError:
321            return None
322
323        for schedule in schedule_list.schedules:
324            if schedule.to_be_deleted:
325                # skip this schedule if it is being deleted, it can as well not exist
326                continue
327            if str(schedule.schedule_id) == self.name:
328                self.name = schedule.name
329                return schedule
330            elif schedule.name == self.name:
331                return schedule
332        return None
333
334    def get_account_id(self):
335        # Validate account id
336        # Return account_id if found, None otherwise
337        try:
338            account_id = self.elementsw_helper.account_exists(self.account_id)
339            return account_id
340        except ApiServerError:
341            return None
342
343    def get_volume_id(self):
344        # Validate volume_ids
345        # Return volume ids if found, fail if not found
346        volume_ids = []
347        for volume in self.volumes:
348            volume_id = self.elementsw_helper.volume_exists(volume.strip(), self.account_id)
349            if volume_id:
350                volume_ids.append(volume_id)
351            else:
352                self.module.fail_json(msg='Specified volume %s does not exist' % volume)
353        return volume_ids
354
355    def get_frequency(self):
356        # Configuring frequency depends on self.schedule_type
357        frequency = None
358        if self.schedule_type is not None and self.schedule_type == 'DaysOfWeekFrequency':
359            if self.weekdays is not None:
360                params = dict(weekdays=self.weekdays)
361                if self.days_of_week_hours is not None:
362                    params['hours'] = self.days_of_week_hours
363                if self.days_of_week_minutes is not None:
364                    params['minutes'] = self.days_of_week_minutes
365                frequency = DaysOfWeekFrequency(**params)
366        elif self.schedule_type is not None and self.schedule_type == 'DaysOfMonthFrequency':
367            if self.days_of_month_monthdays is not None:
368                params = dict(monthdays=self.days_of_month_monthdays)
369                if self.days_of_month_hours is not None:
370                    params['hours'] = self.days_of_month_hours
371                if self.days_of_month_minutes is not None:
372                    params['minutes'] = self.days_of_month_minutes
373                frequency = DaysOfMonthFrequency(**params)
374        elif self.schedule_type is not None and self.schedule_type == 'TimeIntervalFrequency':
375            params = dict()
376            if self.time_interval_days is not None:
377                params['days'] = self.time_interval_days
378            if self.time_interval_hours is not None:
379                params['hours'] = self.time_interval_hours
380            if self.time_interval_minutes is not None:
381                params['minutes'] = self.time_interval_minutes
382            if not params or sum(params.values()) == 0:
383                self.module.fail_json(msg='Specify at least one non zero value with TimeIntervalFrequency.')
384            frequency = TimeIntervalFrequency(**params)
385        return frequency
386
387    def is_same_schedule_type(self, schedule_detail):
388        # To check schedule type is same or not
389        if str(schedule_detail.frequency).split('(')[0] == self.schedule_type:
390            return True
391        else:
392            return False
393
394    def create_schedule(self):
395        # Create schedule
396        try:
397            frequency = self.get_frequency()
398            if frequency is None:
399                self.module.fail_json(msg='Failed to create schedule frequency object - type %s parameters' % self.schedule_type)
400
401            # Create schedule
402            name = self.name
403            schedule_info = ScheduleInfo(
404                volume_ids=self.volumes,
405                snapshot_name=self.snapshot_name,
406                retention=self.retention
407            )
408            if HAS_SF_SDK_1_7:
409                sched = Schedule(frequency, name, schedule_info)
410            else:
411                sched = Schedule(schedule_info, name, frequency)
412            sched.paused = self.paused
413            sched.recurring = self.recurring
414            sched.starting_date = self.starting_date
415
416            self.create_schedule_result = self.sfe.create_schedule(sched)
417
418        except (ApiServerError, ApiConnectionError) as exc:
419            self.module.fail_json(msg='Error creating schedule %s: %s' % (self.name, to_native(exc)),
420                                  exception=traceback.format_exc())
421
422    def delete_schedule(self, schedule_id):
423        # delete schedule
424        try:
425            get_schedule_result = self.sfe.get_schedule(schedule_id=schedule_id)
426            sched = get_schedule_result.schedule
427            sched.to_be_deleted = True
428            self.sfe.modify_schedule(schedule=sched)
429
430        except (ApiServerError, ApiConnectionError) as exc:
431            self.module.fail_json(msg='Error deleting schedule %s: %s' % (self.name, to_native(exc)),
432                                  exception=traceback.format_exc())
433
434    def update_schedule(self, schedule_id):
435        # Update schedule
436        try:
437            get_schedule_result = self.sfe.get_schedule(schedule_id=schedule_id)
438            sched = get_schedule_result.schedule
439            # Update schedule properties
440            sched.frequency = self.get_frequency()
441            if sched.frequency is None:
442                self.module.fail_json(msg='Failed to create schedule frequency object - type %s parameters' % self.schedule_type)
443
444            if self.volumes is not None and len(self.volumes) > 0:
445                sched.schedule_info.volume_ids = self.volumes
446            if self.retention is not None:
447                sched.schedule_info.retention = self.retention
448            if self.snapshot_name is not None:
449                sched.schedule_info.snapshot_name = self.snapshot_name
450            if self.paused is not None:
451                sched.paused = self.paused
452            if self.recurring is not None:
453                sched.recurring = self.recurring
454            if self.starting_date is not None:
455                sched.starting_date = self.starting_date
456
457            # Make API call
458            self.sfe.modify_schedule(schedule=sched)
459
460        except (ApiServerError, ApiConnectionError) as exc:
461            self.module.fail_json(msg='Error updating schedule %s: %s' % (self.name, to_native(exc)),
462                                  exception=traceback.format_exc())
463
464    def apply(self):
465        # Perform pre-checks, call functions and exit
466
467        changed = False
468        update_schedule = False
469
470        if self.account_id is not None:
471            self.account_id = self.get_account_id()
472
473        if self.state == 'present' and self.volumes is not None:
474            if self.account_id:
475                self.volumes = self.get_volume_id()
476            else:
477                self.module.fail_json(msg='Specified account id does not exist')
478
479        # Getting the schedule details
480        schedule_detail = self.get_schedule()
481
482        if schedule_detail is None and self.state == 'present':
483            if len(self.volumes) > 0:
484                changed = True
485            else:
486                self.module.fail_json(msg='Specified volumes not on cluster')
487        elif schedule_detail is not None:
488            # Getting the schedule id
489            if self.state == 'absent':
490                changed = True
491            else:
492                # Check if we need to update the snapshot schedule
493                if self.retention is not None and schedule_detail.schedule_info.retention != self.retention:
494                    update_schedule = True
495                    changed = True
496                elif self.snapshot_name is not None and schedule_detail.schedule_info.snapshot_name != self.snapshot_name:
497                    update_schedule = True
498                    changed = True
499                elif self.paused is not None and schedule_detail.paused != self.paused:
500                    update_schedule = True
501                    changed = True
502                elif self.recurring is not None and schedule_detail.recurring != self.recurring:
503                    update_schedule = True
504                    changed = True
505                elif self.starting_date is not None and schedule_detail.starting_date != self.starting_date:
506                    update_schedule = True
507                    changed = True
508                elif self.volumes is not None and len(self.volumes) > 0:
509                    for volume_id in schedule_detail.schedule_info.volume_ids:
510                        if volume_id not in self.volumes:
511                            update_schedule = True
512                            changed = True
513
514                temp_frequency = self.get_frequency()
515                if temp_frequency is not None:
516                    # Checking schedule_type changes
517                    if self.is_same_schedule_type(schedule_detail):
518                        # If same schedule type
519                        if self.schedule_type == "TimeIntervalFrequency":
520                            # Check if there is any change in schedule.frequency, If schedule_type is time_interval
521                            if schedule_detail.frequency.days != temp_frequency.days or \
522                                    schedule_detail.frequency.hours != temp_frequency.hours or \
523                                    schedule_detail.frequency.minutes != temp_frequency.minutes:
524                                update_schedule = True
525                                changed = True
526                        elif self.schedule_type == "DaysOfMonthFrequency":
527                            # Check if there is any change in schedule.frequency, If schedule_type is days_of_month
528                            if len(schedule_detail.frequency.monthdays) != len(temp_frequency.monthdays) or \
529                                    schedule_detail.frequency.hours != temp_frequency.hours or \
530                                    schedule_detail.frequency.minutes != temp_frequency.minutes:
531                                update_schedule = True
532                                changed = True
533                            elif len(schedule_detail.frequency.monthdays) == len(temp_frequency.monthdays):
534                                actual_frequency_monthday = schedule_detail.frequency.monthdays
535                                temp_frequency_monthday = temp_frequency.monthdays
536                                for monthday in actual_frequency_monthday:
537                                    if monthday not in temp_frequency_monthday:
538                                        update_schedule = True
539                                        changed = True
540                        elif self.schedule_type == "DaysOfWeekFrequency":
541                            # Check if there is any change in schedule.frequency, If schedule_type is days_of_week
542                            if len(schedule_detail.frequency.weekdays) != len(temp_frequency.weekdays) or \
543                                    schedule_detail.frequency.hours != temp_frequency.hours or \
544                                    schedule_detail.frequency.minutes != temp_frequency.minutes:
545                                update_schedule = True
546                                changed = True
547                            elif len(schedule_detail.frequency.weekdays) == len(temp_frequency.weekdays):
548                                actual_frequency_weekdays = schedule_detail.frequency.weekdays
549                                temp_frequency_weekdays = temp_frequency.weekdays
550                                if len([actual_weekday for actual_weekday, temp_weekday in
551                                        zip(actual_frequency_weekdays, temp_frequency_weekdays) if actual_weekday != temp_weekday]) != 0:
552                                    update_schedule = True
553                                    changed = True
554                    else:
555                        update_schedule = True
556                        changed = True
557                else:
558                    self.module.fail_json(msg='Failed to create schedule frequency object - type %s parameters' % self.schedule_type)
559
560        result_message = " "
561        if changed:
562            if self.module.check_mode:
563                # Skip changes
564                result_message = "Check mode, skipping changes"
565            else:
566                if self.state == 'present':
567                    if update_schedule:
568                        self.update_schedule(schedule_detail.schedule_id)
569                        result_message = "Snapshot Schedule modified"
570                    else:
571                        self.create_schedule()
572                        result_message = "Snapshot Schedule created"
573                elif self.state == 'absent':
574                    self.delete_schedule(schedule_detail.schedule_id)
575                    result_message = "Snapshot Schedule deleted"
576
577        self.module.exit_json(changed=changed, msg=result_message)
578
579
580def main():
581    sss = ElementSWSnapShotSchedule()
582    sss.apply()
583
584
585if __name__ == '__main__':
586    main()
587