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