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