1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2012, Dane Summers <dsummers@pinedesk.biz> 5# Copyright: (c) 2013, Mike Grozak <mike.grozak@gmail.com> 6# Copyright: (c) 2013, Patrick Callahan <pmc@patrickcallahan.com> 7# Copyright: (c) 2015, Evan Kaufman <evan@digitalflophouse.com> 8# Copyright: (c) 2015, Luca Berruti <nadirio@gmail.com> 9# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 10 11from __future__ import absolute_import, division, print_function 12__metaclass__ = type 13 14ANSIBLE_METADATA = {'metadata_version': '1.1', 15 'status': ['preview'], 16 'supported_by': 'community'} 17 18DOCUMENTATION = r''' 19--- 20module: cron 21short_description: Manage cron.d and crontab entries 22description: 23 - Use this module to manage crontab and environment variables entries. This module allows 24 you to create environment variables and named crontab entries, update, or delete them. 25 - 'When crontab jobs are managed: the module includes one line with the description of the 26 crontab entry C("#Ansible: <name>") corresponding to the "name" passed to the module, 27 which is used by future ansible/module calls to find/check the state. The "name" 28 parameter should be unique, and changing the "name" value will result in a new cron 29 task being created (or a different one being removed).' 30 - When environment variables are managed, no comment line is added, but, when the module 31 needs to find/check the state, it uses the "name" parameter to find the environment 32 variable definition line. 33 - When using symbols such as %, they must be properly escaped. 34version_added: "0.9" 35options: 36 name: 37 description: 38 - Description of a crontab entry or, if env is set, the name of environment variable. 39 - Required if C(state=absent). 40 - Note that if name is not set and C(state=present), then a 41 new crontab entry will always be created, regardless of existing ones. 42 - This parameter will always be required in future releases. 43 type: str 44 user: 45 description: 46 - The specific user whose crontab should be modified. 47 - When unset, this parameter defaults to using C(root). 48 type: str 49 job: 50 description: 51 - The command to execute or, if env is set, the value of environment variable. 52 - The command should not contain line breaks. 53 - Required if C(state=present). 54 type: str 55 aliases: [ value ] 56 state: 57 description: 58 - Whether to ensure the job or environment variable is present or absent. 59 type: str 60 choices: [ absent, present ] 61 default: present 62 cron_file: 63 description: 64 - If specified, uses this file instead of an individual user's crontab. 65 - If this is a relative path, it is interpreted with respect to I(/etc/cron.d). 66 - If it is absolute, it will typically be I(/etc/crontab). 67 - Many linux distros expect (and some require) the filename portion to consist solely 68 of upper- and lower-case letters, digits, underscores, and hyphens. 69 - To use the C(cron_file) parameter you must specify the C(user) as well. 70 type: str 71 backup: 72 description: 73 - If set, create a backup of the crontab before it is modified. 74 The location of the backup is returned in the C(backup_file) variable by this module. 75 type: bool 76 default: no 77 minute: 78 description: 79 - Minute when the job should run ( 0-59, *, */2, etc ) 80 type: str 81 default: "*" 82 hour: 83 description: 84 - Hour when the job should run ( 0-23, *, */2, etc ) 85 type: str 86 default: "*" 87 day: 88 description: 89 - Day of the month the job should run ( 1-31, *, */2, etc ) 90 type: str 91 default: "*" 92 aliases: [ dom ] 93 month: 94 description: 95 - Month of the year the job should run ( 1-12, *, */2, etc ) 96 type: str 97 default: "*" 98 weekday: 99 description: 100 - Day of the week that the job should run ( 0-6 for Sunday-Saturday, *, etc ) 101 type: str 102 default: "*" 103 aliases: [ dow ] 104 reboot: 105 description: 106 - If the job should be run at reboot. This option is deprecated. Users should use special_time. 107 version_added: "1.0" 108 type: bool 109 default: no 110 special_time: 111 description: 112 - Special time specification nickname. 113 type: str 114 choices: [ annually, daily, hourly, monthly, reboot, weekly, yearly ] 115 version_added: "1.3" 116 disabled: 117 description: 118 - If the job should be disabled (commented out) in the crontab. 119 - Only has effect if C(state=present). 120 type: bool 121 default: no 122 version_added: "2.0" 123 env: 124 description: 125 - If set, manages a crontab's environment variable. 126 - New variables are added on top of crontab. 127 - C(name) and C(value) parameters are the name and the value of environment variable. 128 type: bool 129 default: no 130 version_added: "2.1" 131 insertafter: 132 description: 133 - Used with C(state=present) and C(env). 134 - If specified, the environment variable will be inserted after the declaration of specified environment variable. 135 type: str 136 version_added: "2.1" 137 insertbefore: 138 description: 139 - Used with C(state=present) and C(env). 140 - If specified, the environment variable will be inserted before the declaration of specified environment variable. 141 type: str 142 version_added: "2.1" 143requirements: 144 - cron 145author: 146 - Dane Summers (@dsummersl) 147 - Mike Grozak (@rhaido) 148 - Patrick Callahan (@dirtyharrycallahan) 149 - Evan Kaufman (@EvanK) 150 - Luca Berruti (@lberruti) 151''' 152 153EXAMPLES = r''' 154- name: Ensure a job that runs at 2 and 5 exists. Creates an entry like "0 5,2 * * ls -alh > /dev/null" 155 cron: 156 name: "check dirs" 157 minute: "0" 158 hour: "5,2" 159 job: "ls -alh > /dev/null" 160 161- name: 'Ensure an old job is no longer present. Removes any job that is prefixed by "#Ansible: an old job" from the crontab' 162 cron: 163 name: "an old job" 164 state: absent 165 166- name: Creates an entry like "@reboot /some/job.sh" 167 cron: 168 name: "a job for reboot" 169 special_time: reboot 170 job: "/some/job.sh" 171 172- name: Creates an entry like "PATH=/opt/bin" on top of crontab 173 cron: 174 name: PATH 175 env: yes 176 job: /opt/bin 177 178- name: Creates an entry like "APP_HOME=/srv/app" and insert it after PATH declaration 179 cron: 180 name: APP_HOME 181 env: yes 182 job: /srv/app 183 insertafter: PATH 184 185- name: Creates a cron file under /etc/cron.d 186 cron: 187 name: yum autoupdate 188 weekday: "2" 189 minute: "0" 190 hour: "12" 191 user: root 192 job: "YUMINTERACTIVE=0 /usr/sbin/yum-autoupdate" 193 cron_file: ansible_yum-autoupdate 194 195- name: Removes a cron file from under /etc/cron.d 196 cron: 197 name: "yum autoupdate" 198 cron_file: ansible_yum-autoupdate 199 state: absent 200 201- name: Removes "APP_HOME" environment variable from crontab 202 cron: 203 name: APP_HOME 204 env: yes 205 state: absent 206''' 207 208import os 209import platform 210import pwd 211import re 212import sys 213import tempfile 214 215from ansible.module_utils.basic import AnsibleModule, get_platform 216from ansible.module_utils.common.text.converters import to_bytes, to_native 217from ansible.module_utils.six.moves import shlex_quote 218 219 220class CronTabError(Exception): 221 pass 222 223 224class CronTab(object): 225 """ 226 CronTab object to write time based crontab file 227 228 user - the user of the crontab (defaults to root) 229 cron_file - a cron file under /etc/cron.d, or an absolute path 230 """ 231 232 def __init__(self, module, user=None, cron_file=None): 233 self.module = module 234 self.user = user 235 self.root = (os.getuid() == 0) 236 self.lines = None 237 self.ansible = "#Ansible: " 238 self.n_existing = '' 239 self.cron_cmd = self.module.get_bin_path('crontab', required=True) 240 241 if cron_file: 242 if os.path.isabs(cron_file): 243 self.cron_file = cron_file 244 self.b_cron_file = to_bytes(cron_file, errors='surrogate_or_strict') 245 else: 246 self.cron_file = os.path.join('/etc/cron.d', cron_file) 247 self.b_cron_file = os.path.join(b'/etc/cron.d', to_bytes(cron_file, errors='surrogate_or_strict')) 248 else: 249 self.cron_file = None 250 251 self.read() 252 253 def read(self): 254 # Read in the crontab from the system 255 self.lines = [] 256 if self.cron_file: 257 # read the cronfile 258 try: 259 f = open(self.b_cron_file, 'rb') 260 self.n_existing = to_native(f.read(), errors='surrogate_or_strict') 261 self.lines = self.n_existing.splitlines() 262 f.close() 263 except IOError: 264 # cron file does not exist 265 return 266 except Exception: 267 raise CronTabError("Unexpected error:", sys.exc_info()[0]) 268 else: 269 # using safely quoted shell for now, but this really should be two non-shell calls instead. FIXME 270 (rc, out, err) = self.module.run_command(self._read_user_execute(), use_unsafe_shell=True) 271 272 if rc != 0 and rc != 1: # 1 can mean that there are no jobs. 273 raise CronTabError("Unable to read crontab") 274 275 self.n_existing = out 276 277 lines = out.splitlines() 278 count = 0 279 for l in lines: 280 if count > 2 or (not re.match(r'# DO NOT EDIT THIS FILE - edit the master and reinstall.', l) and 281 not re.match(r'# \(/tmp/.*installed on.*\)', l) and 282 not re.match(r'# \(.*version.*\)', l)): 283 self.lines.append(l) 284 else: 285 pattern = re.escape(l) + '[\r\n]?' 286 self.n_existing = re.sub(pattern, '', self.n_existing, 1) 287 count += 1 288 289 def is_empty(self): 290 if len(self.lines) == 0: 291 return True 292 else: 293 return False 294 295 def write(self, backup_file=None): 296 """ 297 Write the crontab to the system. Saves all information. 298 """ 299 if backup_file: 300 fileh = open(backup_file, 'wb') 301 elif self.cron_file: 302 fileh = open(self.b_cron_file, 'wb') 303 else: 304 filed, path = tempfile.mkstemp(prefix='crontab') 305 os.chmod(path, int('0644', 8)) 306 fileh = os.fdopen(filed, 'wb') 307 308 fileh.write(to_bytes(self.render())) 309 fileh.close() 310 311 # return if making a backup 312 if backup_file: 313 return 314 315 # Add the entire crontab back to the user crontab 316 if not self.cron_file: 317 # quoting shell args for now but really this should be two non-shell calls. FIXME 318 (rc, out, err) = self.module.run_command(self._write_execute(path), use_unsafe_shell=True) 319 os.unlink(path) 320 321 if rc != 0: 322 self.module.fail_json(msg=err) 323 324 # set SELinux permissions 325 if self.module.selinux_enabled() and self.cron_file: 326 self.module.set_default_selinux_context(self.cron_file, False) 327 328 def do_comment(self, name): 329 return "%s%s" % (self.ansible, name) 330 331 def add_job(self, name, job): 332 # Add the comment 333 self.lines.append(self.do_comment(name)) 334 335 # Add the job 336 self.lines.append("%s" % (job)) 337 338 def update_job(self, name, job): 339 return self._update_job(name, job, self.do_add_job) 340 341 def do_add_job(self, lines, comment, job): 342 lines.append(comment) 343 344 lines.append("%s" % (job)) 345 346 def remove_job(self, name): 347 return self._update_job(name, "", self.do_remove_job) 348 349 def do_remove_job(self, lines, comment, job): 350 return None 351 352 def add_env(self, decl, insertafter=None, insertbefore=None): 353 if not (insertafter or insertbefore): 354 self.lines.insert(0, decl) 355 return 356 357 if insertafter: 358 other_name = insertafter 359 elif insertbefore: 360 other_name = insertbefore 361 other_decl = self.find_env(other_name) 362 if len(other_decl) > 0: 363 if insertafter: 364 index = other_decl[0] + 1 365 elif insertbefore: 366 index = other_decl[0] 367 self.lines.insert(index, decl) 368 return 369 370 self.module.fail_json(msg="Variable named '%s' not found." % other_name) 371 372 def update_env(self, name, decl): 373 return self._update_env(name, decl, self.do_add_env) 374 375 def do_add_env(self, lines, decl): 376 lines.append(decl) 377 378 def remove_env(self, name): 379 return self._update_env(name, '', self.do_remove_env) 380 381 def do_remove_env(self, lines, decl): 382 return None 383 384 def remove_job_file(self): 385 try: 386 os.unlink(self.cron_file) 387 return True 388 except OSError: 389 # cron file does not exist 390 return False 391 except Exception: 392 raise CronTabError("Unexpected error:", sys.exc_info()[0]) 393 394 def find_job(self, name, job=None): 395 # attempt to find job by 'Ansible:' header comment 396 comment = None 397 for l in self.lines: 398 if comment is not None: 399 if comment == name: 400 return [comment, l] 401 else: 402 comment = None 403 elif re.match(r'%s' % self.ansible, l): 404 comment = re.sub(r'%s' % self.ansible, '', l) 405 406 # failing that, attempt to find job by exact match 407 if job: 408 for i, l in enumerate(self.lines): 409 if l == job: 410 # if no leading ansible header, insert one 411 if not re.match(r'%s' % self.ansible, self.lines[i - 1]): 412 self.lines.insert(i, self.do_comment(name)) 413 return [self.lines[i], l, True] 414 # if a leading blank ansible header AND job has a name, update header 415 elif name and self.lines[i - 1] == self.do_comment(None): 416 self.lines[i - 1] = self.do_comment(name) 417 return [self.lines[i - 1], l, True] 418 419 return [] 420 421 def find_env(self, name): 422 for index, l in enumerate(self.lines): 423 if re.match(r'^%s=' % name, l): 424 return [index, l] 425 426 return [] 427 428 def get_cron_job(self, minute, hour, day, month, weekday, job, special, disabled): 429 # normalize any leading/trailing newlines (ansible/ansible-modules-core#3791) 430 job = job.strip('\r\n') 431 432 if disabled: 433 disable_prefix = '#' 434 else: 435 disable_prefix = '' 436 437 if special: 438 if self.cron_file: 439 return "%s@%s %s %s" % (disable_prefix, special, self.user, job) 440 else: 441 return "%s@%s %s" % (disable_prefix, special, job) 442 else: 443 if self.cron_file: 444 return "%s%s %s %s %s %s %s %s" % (disable_prefix, minute, hour, day, month, weekday, self.user, job) 445 else: 446 return "%s%s %s %s %s %s %s" % (disable_prefix, minute, hour, day, month, weekday, job) 447 448 def get_jobnames(self): 449 jobnames = [] 450 451 for l in self.lines: 452 if re.match(r'%s' % self.ansible, l): 453 jobnames.append(re.sub(r'%s' % self.ansible, '', l)) 454 455 return jobnames 456 457 def get_envnames(self): 458 envnames = [] 459 460 for l in self.lines: 461 if re.match(r'^\S+=', l): 462 envnames.append(l.split('=')[0]) 463 464 return envnames 465 466 def _update_job(self, name, job, addlinesfunction): 467 ansiblename = self.do_comment(name) 468 newlines = [] 469 comment = None 470 471 for l in self.lines: 472 if comment is not None: 473 addlinesfunction(newlines, comment, job) 474 comment = None 475 elif l == ansiblename: 476 comment = l 477 else: 478 newlines.append(l) 479 480 self.lines = newlines 481 482 if len(newlines) == 0: 483 return True 484 else: 485 return False # TODO add some more error testing 486 487 def _update_env(self, name, decl, addenvfunction): 488 newlines = [] 489 490 for l in self.lines: 491 if re.match(r'^%s=' % name, l): 492 addenvfunction(newlines, decl) 493 else: 494 newlines.append(l) 495 496 self.lines = newlines 497 498 def render(self): 499 """ 500 Render this crontab as it would be in the crontab. 501 """ 502 crons = [] 503 for cron in self.lines: 504 crons.append(cron) 505 506 result = '\n'.join(crons) 507 if result: 508 result = result.rstrip('\r\n') + '\n' 509 return result 510 511 def _read_user_execute(self): 512 """ 513 Returns the command line for reading a crontab 514 """ 515 user = '' 516 if self.user: 517 if platform.system() == 'SunOS': 518 return "su %s -c '%s -l'" % (shlex_quote(self.user), shlex_quote(self.cron_cmd)) 519 elif platform.system() == 'AIX': 520 return "%s -l %s" % (shlex_quote(self.cron_cmd), shlex_quote(self.user)) 521 elif platform.system() == 'HP-UX': 522 return "%s %s %s" % (self.cron_cmd, '-l', shlex_quote(self.user)) 523 elif pwd.getpwuid(os.getuid())[0] != self.user: 524 user = '-u %s' % shlex_quote(self.user) 525 return "%s %s %s" % (self.cron_cmd, user, '-l') 526 527 def _write_execute(self, path): 528 """ 529 Return the command line for writing a crontab 530 """ 531 user = '' 532 if self.user: 533 if platform.system() in ['SunOS', 'HP-UX', 'AIX']: 534 return "chown %s %s ; su '%s' -c '%s %s'" % ( 535 shlex_quote(self.user), shlex_quote(path), shlex_quote(self.user), self.cron_cmd, shlex_quote(path)) 536 elif pwd.getpwuid(os.getuid())[0] != self.user: 537 user = '-u %s' % shlex_quote(self.user) 538 return "%s %s %s" % (self.cron_cmd, user, shlex_quote(path)) 539 540 541def main(): 542 # The following example playbooks: 543 # 544 # - cron: name="check dirs" hour="5,2" job="ls -alh > /dev/null" 545 # 546 # - name: do the job 547 # cron: name="do the job" hour="5,2" job="/some/dir/job.sh" 548 # 549 # - name: no job 550 # cron: name="an old job" state=absent 551 # 552 # - name: sets env 553 # cron: name="PATH" env=yes value="/bin:/usr/bin" 554 # 555 # Would produce: 556 # PATH=/bin:/usr/bin 557 # # Ansible: check dirs 558 # * * 5,2 * * ls -alh > /dev/null 559 # # Ansible: do the job 560 # * * 5,2 * * /some/dir/job.sh 561 562 module = AnsibleModule( 563 argument_spec=dict( 564 name=dict(type='str'), 565 user=dict(type='str'), 566 job=dict(type='str', aliases=['value']), 567 cron_file=dict(type='str'), 568 state=dict(type='str', default='present', choices=['present', 'absent']), 569 backup=dict(type='bool', default=False), 570 minute=dict(type='str', default='*'), 571 hour=dict(type='str', default='*'), 572 day=dict(type='str', default='*', aliases=['dom']), 573 month=dict(type='str', default='*'), 574 weekday=dict(type='str', default='*', aliases=['dow']), 575 reboot=dict(type='bool', default=False), 576 special_time=dict(type='str', choices=["reboot", "yearly", "annually", "monthly", "weekly", "daily", "hourly"]), 577 disabled=dict(type='bool', default=False), 578 env=dict(type='bool'), 579 insertafter=dict(type='str'), 580 insertbefore=dict(type='str'), 581 ), 582 supports_check_mode=True, 583 mutually_exclusive=[ 584 ['reboot', 'special_time'], 585 ['insertafter', 'insertbefore'], 586 ], 587 ) 588 589 name = module.params['name'] 590 user = module.params['user'] 591 job = module.params['job'] 592 cron_file = module.params['cron_file'] 593 state = module.params['state'] 594 backup = module.params['backup'] 595 minute = module.params['minute'] 596 hour = module.params['hour'] 597 day = module.params['day'] 598 month = module.params['month'] 599 weekday = module.params['weekday'] 600 reboot = module.params['reboot'] 601 special_time = module.params['special_time'] 602 disabled = module.params['disabled'] 603 env = module.params['env'] 604 insertafter = module.params['insertafter'] 605 insertbefore = module.params['insertbefore'] 606 do_install = state == 'present' 607 608 changed = False 609 res_args = dict() 610 warnings = list() 611 612 if cron_file: 613 cron_file_basename = os.path.basename(cron_file) 614 if not re.search(r'^[A-Z0-9_-]+$', cron_file_basename, re.I): 615 warnings.append('Filename portion of cron_file ("%s") should consist' % cron_file_basename + 616 ' solely of upper- and lower-case letters, digits, underscores, and hyphens') 617 618 # Ensure all files generated are only writable by the owning user. Primarily relevant for the cron_file option. 619 os.umask(int('022', 8)) 620 crontab = CronTab(module, user, cron_file) 621 622 module.debug('cron instantiated - name: "%s"' % name) 623 624 if not name: 625 module.deprecate( 626 msg="The 'name' parameter will be required in future releases.", 627 version='2.12' 628 ) 629 if reboot: 630 module.deprecate( 631 msg="The 'reboot' parameter will be removed in future releases. Use 'special_time' option instead.", 632 version='2.12' 633 ) 634 635 if module._diff: 636 diff = dict() 637 diff['before'] = crontab.n_existing 638 if crontab.cron_file: 639 diff['before_header'] = crontab.cron_file 640 else: 641 if crontab.user: 642 diff['before_header'] = 'crontab for user "%s"' % crontab.user 643 else: 644 diff['before_header'] = 'crontab' 645 646 # --- user input validation --- 647 648 if (special_time or reboot) and \ 649 (True in [(x != '*') for x in [minute, hour, day, month, weekday]]): 650 module.fail_json(msg="You must specify time and date fields or special time.") 651 652 # cannot support special_time on solaris 653 if (special_time or reboot) and get_platform() == 'SunOS': 654 module.fail_json(msg="Solaris does not support special_time=... or @reboot") 655 656 if cron_file and do_install: 657 if not user: 658 module.fail_json(msg="To use cron_file=... parameter you must specify user=... as well") 659 660 if job is None and do_install: 661 module.fail_json(msg="You must specify 'job' to install a new cron job or variable") 662 663 if (insertafter or insertbefore) and not env and do_install: 664 module.fail_json(msg="Insertafter and insertbefore parameters are valid only with env=yes") 665 666 if reboot: 667 special_time = "reboot" 668 669 # if requested make a backup before making a change 670 if backup and not module.check_mode: 671 (backuph, backup_file) = tempfile.mkstemp(prefix='crontab') 672 crontab.write(backup_file) 673 674 if crontab.cron_file and not name and not do_install: 675 if module._diff: 676 diff['after'] = '' 677 diff['after_header'] = '/dev/null' 678 else: 679 diff = dict() 680 if module.check_mode: 681 changed = os.path.isfile(crontab.cron_file) 682 else: 683 changed = crontab.remove_job_file() 684 module.exit_json(changed=changed, cron_file=cron_file, state=state, diff=diff) 685 686 if env: 687 if ' ' in name: 688 module.fail_json(msg="Invalid name for environment variable") 689 decl = '%s="%s"' % (name, job) 690 old_decl = crontab.find_env(name) 691 692 if do_install: 693 if len(old_decl) == 0: 694 crontab.add_env(decl, insertafter, insertbefore) 695 changed = True 696 if len(old_decl) > 0 and old_decl[1] != decl: 697 crontab.update_env(name, decl) 698 changed = True 699 else: 700 if len(old_decl) > 0: 701 crontab.remove_env(name) 702 changed = True 703 else: 704 if do_install: 705 for char in ['\r', '\n']: 706 if char in job.strip('\r\n'): 707 warnings.append('Job should not contain line breaks') 708 break 709 710 job = crontab.get_cron_job(minute, hour, day, month, weekday, job, special_time, disabled) 711 old_job = crontab.find_job(name, job) 712 713 if len(old_job) == 0: 714 crontab.add_job(name, job) 715 changed = True 716 if len(old_job) > 0 and old_job[1] != job: 717 crontab.update_job(name, job) 718 changed = True 719 if len(old_job) > 2: 720 crontab.update_job(name, job) 721 changed = True 722 else: 723 old_job = crontab.find_job(name) 724 725 if len(old_job) > 0: 726 crontab.remove_job(name) 727 changed = True 728 729 # no changes to env/job, but existing crontab needs a terminating newline 730 if not changed and crontab.n_existing != '': 731 if not (crontab.n_existing.endswith('\r') or crontab.n_existing.endswith('\n')): 732 changed = True 733 734 res_args = dict( 735 jobs=crontab.get_jobnames(), 736 envs=crontab.get_envnames(), 737 warnings=warnings, 738 changed=changed 739 ) 740 741 if changed: 742 if not module.check_mode: 743 crontab.write() 744 if module._diff: 745 diff['after'] = crontab.render() 746 if crontab.cron_file: 747 diff['after_header'] = crontab.cron_file 748 else: 749 if crontab.user: 750 diff['after_header'] = 'crontab for user "%s"' % crontab.user 751 else: 752 diff['after_header'] = 'crontab' 753 754 res_args['diff'] = diff 755 756 # retain the backup only if crontab or cron file have changed 757 if backup and not module.check_mode: 758 if changed: 759 res_args['backup_file'] = backup_file 760 else: 761 os.unlink(backup_file) 762 763 if cron_file: 764 res_args['cron_file'] = cron_file 765 766 module.exit_json(**res_args) 767 768 # --- should never get here 769 module.exit_json(msg="Unable to execute cron task.") 770 771 772if __name__ == '__main__': 773 main() 774