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