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