# Buggy code
def main():
    # The following example playbooks:
    #
    # - cron: name="check dirs" hour="5,2" job="ls -alh > /dev/null"
    #
    # - name: do the job
    #   cron: name="do the job" hour="5,2" job="/some/dir/job.sh"
    #
    # - name: no job
    #   cron: name="an old job" state=absent
    #
    # - name: sets env
    #   cron: name="PATH" env=yes value="/bin:/usr/bin"
    #
    # Would produce:
    # PATH=/bin:/usr/bin
    # # Ansible: check dirs
    # * * 5,2 * * ls -alh > /dev/null
    # # Ansible: do the job
    # * * 5,2 * * /some/dir/job.sh

    module = AnsibleModule(
        argument_spec=dict(
            name=dict(type="str"),
            user=dict(type="str"),
            job=dict(type="str", aliases=["value"]),
            cron_file=dict(type="str"),
            state=dict(type="str", default="present", choices=["present", "absent"]),
            backup=dict(type="bool", default=False),
            minute=dict(type="str", default="*"),
            hour=dict(type="str", default="*"),
            day=dict(type="str", default="*", aliases=["dom"]),
            month=dict(type="str", default="*"),
            weekday=dict(type="str", default="*", aliases=["dow"]),
            reboot=dict(type="bool", default=False),
            special_time=dict(
                type="str",
                choices=[
                    "reboot",
                    "yearly",
                    "annually",
                    "monthly",
                    "weekly",
                    "daily",
                    "hourly",
                ],
            ),
            disabled=dict(type="bool", default=False),
            env=dict(type="bool"),
            insertafter=dict(type="str"),
            insertbefore=dict(type="str"),
        ),
        supports_check_mode=True,
        mutually_exclusive=[
            ["reboot", "special_time"],
            ["insertafter", "insertbefore"],
        ],
    )

    name = module.params["name"]
    user = module.params["user"]
    job = module.params["job"]
    cron_file = module.params["cron_file"]
    state = module.params["state"]
    backup = module.params["backup"]
    minute = module.params["minute"]
    hour = module.params["hour"]
    day = module.params["day"]
    month = module.params["month"]
    weekday = module.params["weekday"]
    reboot = module.params["reboot"]
    special_time = module.params["special_time"]
    disabled = module.params["disabled"]
    env = module.params["env"]
    insertafter = module.params["insertafter"]
    insertbefore = module.params["insertbefore"]
    do_install = state == "present"

    changed = False
    res_args = dict()
    warnings = list()

    if cron_file:
        cron_file_basename = os.path.basename(cron_file)
        if not re.search(r"^[A-Z0-9_-]+$", cron_file_basename, re.I):
            warnings.append(
                'Filename portion of cron_file ("%s") should consist'
                % cron_file_basename
                + " solely of upper- and lower-case letters, digits, underscores, and hyphens"
            )

    # Ensure all files generated are only writable by the owning user.  Primarily relevant for the cron_file option.
    os.umask(int("022", 8))
    crontab = CronTab(module, user, cron_file)

    module.debug('cron instantiated - name: "%s"' % name)

    if not name:
        module.deprecate(
            msg="The 'name' parameter will be required in future releases.",
            version="2.12",
            collection_name="ansible.builtin",
        )
    if reboot:
        module.deprecate(
            msg="The 'reboot' parameter will be removed in future releases. Use 'special_time' option instead.",
            version="2.12",
            collection_name="ansible.builtin",
        )

    if module._diff:
        diff = dict()
        diff["before"] = crontab.existing
        if crontab.cron_file:
            diff["before_header"] = crontab.cron_file
        else:
            if crontab.user:
                diff["before_header"] = 'crontab for user "%s"' % crontab.user
            else:
                diff["before_header"] = "crontab"

    # --- user input validation ---

    if env and not name:
        module.fail_json(
            msg="You must specify 'name' while working with environment variables (env=yes)"
        )

    if (special_time or reboot) and (
        True in [(x != "*") for x in [minute, hour, day, month, weekday]]
    ):
        module.fail_json(msg="You must specify time and date fields or special time.")

    # cannot support special_time on solaris
    if (special_time or reboot) and platform.system() == "SunOS":
        module.fail_json(msg="Solaris does not support special_time=... or @reboot")

    if cron_file and do_install:
        if not user:
            module.fail_json(
                msg="To use cron_file=... parameter you must specify user=... as well"
            )

    if job is None and do_install:
        module.fail_json(
            msg="You must specify 'job' to install a new cron job or variable"
        )

    if (insertafter or insertbefore) and not env and do_install:
        module.fail_json(
            msg="Insertafter and insertbefore parameters are valid only with env=yes"
        )

    if reboot:
        special_time = "reboot"

    # if requested make a backup before making a change
    if backup and not module.check_mode:
        (backuph, backup_file) = tempfile.mkstemp(prefix="crontab")
        crontab.write(backup_file)

    if crontab.cron_file and not do_install:
        if module._diff:
            diff["after"] = ""
            diff["after_header"] = "/dev/null"
        else:
            diff = dict()
        if module.check_mode:
            changed = os.path.isfile(crontab.cron_file)
        else:
            changed = crontab.remove_job_file()
        module.exit_json(changed=changed, cron_file=cron_file, state=state, diff=diff)

    if env:
        if " " in name:
            module.fail_json(msg="Invalid name for environment variable")
        decl = '%s="%s"' % (name, job)
        old_decl = crontab.find_env(name)

        if do_install:
            if len(old_decl) == 0:
                crontab.add_env(decl, insertafter, insertbefore)
                changed = True
            if len(old_decl) > 0 and old_decl[1] != decl:
                crontab.update_env(name, decl)
                changed = True
        else:
            if len(old_decl) > 0:
                crontab.remove_env(name)
                changed = True
    else:
        if do_install:
            for char in ["\r", "\n"]:
                if char in job.strip("\r\n"):
                    warnings.append("Job should not contain line breaks")
                    break

            job = crontab.get_cron_job(
                minute, hour, day, month, weekday, job, special_time, disabled
            )
            old_job = crontab.find_job(name, job)

            if len(old_job) == 0:
                crontab.add_job(name, job)
                changed = True
            if len(old_job) > 0 and old_job[1] != job:
                crontab.update_job(name, job)
                changed = True
            if len(old_job) > 2:
                crontab.update_job(name, job)
                changed = True
        else:
            old_job = crontab.find_job(name)

            if len(old_job) > 0:
                crontab.remove_job(name)
                changed = True

    # no changes to env/job, but existing crontab needs a terminating newline
    if not changed and crontab.existing != "":
        if not (crontab.existing.endswith("\r") or crontab.existing.endswith("\n")):
            changed = True

    res_args = dict(
        jobs=crontab.get_jobnames(),
        envs=crontab.get_envnames(),
        warnings=warnings,
        changed=changed,
    )

    if changed:
        if not module.check_mode:
            crontab.write()
        if module._diff:
            diff["after"] = crontab.render()
            if crontab.cron_file:
                diff["after_header"] = crontab.cron_file
            else:
                if crontab.user:
                    diff["after_header"] = 'crontab for user "%s"' % crontab.user
                else:
                    diff["after_header"] = "crontab"

            res_args["diff"] = diff

    # retain the backup only if crontab or cron file have changed
    if backup and not module.check_mode:
        if changed:
            res_args["backup_file"] = backup_file
        else:
            os.unlink(backup_file)

    if cron_file:
        res_args["cron_file"] = cron_file

    module.exit_json(**res_args)

    # --- should never get here
    module.exit_json(msg="Unable to execute cron task.")


# Traceback (UnicodeDecodeError)
The full traceback is:
Traceback (most recent call last):
File "/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py", line 257, in read
File "/usr/lib/python3.6/encodings/ascii.py", line 26, in decode
return codecs.ascii_decode(input, self.errors)[0]
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 11: ordinal not in range(128)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/root/.ansible/tmp/ansible-tmp-1589383434.7764497-217077099734091/AnsiballZ_cron.py", line 102, in <module>
_ansiballz_main()
File "/root/.ansible/tmp/ansible-tmp-1589383434.7764497-217077099734091/AnsiballZ_cron.py", line 94, in _ansiballz_main
invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)
File "/root/.ansible/tmp/ansible-tmp-1589383434.7764497-217077099734091/AnsiballZ_cron.py", line 40, in invoke_module
runpy.run_module(mod_name='ansible.modules.system.cron', init_globals=None, run_name='__main__', alter_sys=True)
File "/usr/lib/python3.6/runpy.py", line 205, in run_module
return _run_module_code(code, init_globals, run_name, mod_spec)
File "/usr/lib/python3.6/runpy.py", line 96, in _run_module_code
mod_name, mod_spec, pkg_name, script_name)
File "/usr/lib/python3.6/runpy.py", line 85, in _run_code
exec(code, run_globals)
File "/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py", line 770, in <module>
File "/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py", line 617, in main
File "/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py", line 248, in __init__
File "/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py", line 264, in read
__main__.CronTabError: ('Unexpected error:', <class 'UnicodeDecodeError'>)
failed: [cco-web-uat] (item={'name': 'run-all-tasks', 'path': '/scripts/run-all-tasks.php'}) => {
"ansible_loop_var": "cron_task",
"changed": false,
"cron_task": {
"name": "run-all-tasks",
"path": "/scripts/run-all-tasks.php"
},
"module_stderr": "Shared connection to server.fr closed.\r\n",
"module_stdout": "Traceback (most recent call last):\r\n  File \"/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py\", line 257, in read\r\n  File \"/usr/lib/python3.6/encodings/ascii.py\", line 26, in decode\r\n    return codecs.ascii_decode(input, self.errors)[0]\r\nUnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 11: ordinal not in range(128)\r\n\r\nDuring handling of the above exception, another exception occurred:\r\n\r\nTraceback (most recent call last):\r\n  File \"/root/.ansible/tmp/ansible-tmp-1589383434.7764497-217077099734091/AnsiballZ_cron.py\", line 102, in <module>\r\n    _ansiballz_main()\r\n  File \"/root/.ansible/tmp/ansible-tmp-1589383434.7764497-217077099734091/AnsiballZ_cron.py\", line 94, in _ansiballz_main\r\n    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)\r\n  File \"/root/.ansible/tmp/ansible-tmp-1589383434.7764497-217077099734091/AnsiballZ_cron.py\", line 40, in invoke_module\r\n    runpy.run_module(mod_name='ansible.modules.system.cron', init_globals=None, run_name='__main__', alter_sys=True)\r\n  File \"/usr/lib/python3.6/runpy.py\", line 205, in run_module\r\n    return _run_module_code(code, init_globals, run_name, mod_spec)\r\n  File \"/usr/lib/python3.6/runpy.py\", line 96, in _run_module_code\r\n    mod_name, mod_spec, pkg_name, script_name)\r\n  File \"/usr/lib/python3.6/runpy.py\", line 85, in _run_code\r\n    exec(code, run_globals)\r\n  File \"/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py\", line 770, in <module>\r\n  File \"/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py\", line 617, in main\r\n  File \"/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py\", line 248, in __init__\r\n  File \"/tmp/ansible_cron_payload_0y8i2pgj/ansible_cron_payload.zip/ansible/modules/system/cron.py\", line 264, in read\r\n__main__.CronTabError: ('Unexpected error:', <class 'UnicodeDecodeError'>)\r\n",
"msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
"rc": 1
}

# Fixed code
def main():
    # The following example playbooks:
    #
    # - cron: name="check dirs" hour="5,2" job="ls -alh > /dev/null"
    #
    # - name: do the job
    #   cron: name="do the job" hour="5,2" job="/some/dir/job.sh"
    #
    # - name: no job
    #   cron: name="an old job" state=absent
    #
    # - name: sets env
    #   cron: name="PATH" env=yes value="/bin:/usr/bin"
    #
    # Would produce:
    # PATH=/bin:/usr/bin
    # # Ansible: check dirs
    # * * 5,2 * * ls -alh > /dev/null
    # # Ansible: do the job
    # * * 5,2 * * /some/dir/job.sh

    module = AnsibleModule(
        argument_spec=dict(
            name=dict(type="str"),
            user=dict(type="str"),
            job=dict(type="str", aliases=["value"]),
            cron_file=dict(type="str"),
            state=dict(type="str", default="present", choices=["present", "absent"]),
            backup=dict(type="bool", default=False),
            minute=dict(type="str", default="*"),
            hour=dict(type="str", default="*"),
            day=dict(type="str", default="*", aliases=["dom"]),
            month=dict(type="str", default="*"),
            weekday=dict(type="str", default="*", aliases=["dow"]),
            reboot=dict(type="bool", default=False),
            special_time=dict(
                type="str",
                choices=[
                    "reboot",
                    "yearly",
                    "annually",
                    "monthly",
                    "weekly",
                    "daily",
                    "hourly",
                ],
            ),
            disabled=dict(type="bool", default=False),
            env=dict(type="bool"),
            insertafter=dict(type="str"),
            insertbefore=dict(type="str"),
        ),
        supports_check_mode=True,
        mutually_exclusive=[
            ["reboot", "special_time"],
            ["insertafter", "insertbefore"],
        ],
    )

    name = module.params["name"]
    user = module.params["user"]
    job = module.params["job"]
    cron_file = module.params["cron_file"]
    state = module.params["state"]
    backup = module.params["backup"]
    minute = module.params["minute"]
    hour = module.params["hour"]
    day = module.params["day"]
    month = module.params["month"]
    weekday = module.params["weekday"]
    reboot = module.params["reboot"]
    special_time = module.params["special_time"]
    disabled = module.params["disabled"]
    env = module.params["env"]
    insertafter = module.params["insertafter"]
    insertbefore = module.params["insertbefore"]
    do_install = state == "present"

    changed = False
    res_args = dict()
    warnings = list()

    if cron_file:
        cron_file_basename = os.path.basename(cron_file)
        if not re.search(r"^[A-Z0-9_-]+$", cron_file_basename, re.I):
            warnings.append(
                'Filename portion of cron_file ("%s") should consist'
                % cron_file_basename
                + " solely of upper- and lower-case letters, digits, underscores, and hyphens"
            )

    # Ensure all files generated are only writable by the owning user.  Primarily relevant for the cron_file option.
    os.umask(int("022", 8))
    crontab = CronTab(module, user, cron_file)

    module.debug('cron instantiated - name: "%s"' % name)

    if not name:
        module.deprecate(
            msg="The 'name' parameter will be required in future releases.",
            version="2.12",
            collection_name="ansible.builtin",
        )
    if reboot:
        module.deprecate(
            msg="The 'reboot' parameter will be removed in future releases. Use 'special_time' option instead.",
            version="2.12",
            collection_name="ansible.builtin",
        )

    if module._diff:
        diff = dict()
        diff["before"] = crontab.n_existing
        if crontab.cron_file:
            diff["before_header"] = crontab.cron_file
        else:
            if crontab.user:
                diff["before_header"] = 'crontab for user "%s"' % crontab.user
            else:
                diff["before_header"] = "crontab"

    # --- user input validation ---

    if env and not name:
        module.fail_json(
            msg="You must specify 'name' while working with environment variables (env=yes)"
        )

    if (special_time or reboot) and (
        True in [(x != "*") for x in [minute, hour, day, month, weekday]]
    ):
        module.fail_json(msg="You must specify time and date fields or special time.")

    # cannot support special_time on solaris
    if (special_time or reboot) and platform.system() == "SunOS":
        module.fail_json(msg="Solaris does not support special_time=... or @reboot")

    if cron_file and do_install:
        if not user:
            module.fail_json(
                msg="To use cron_file=... parameter you must specify user=... as well"
            )

    if job is None and do_install:
        module.fail_json(
            msg="You must specify 'job' to install a new cron job or variable"
        )

    if (insertafter or insertbefore) and not env and do_install:
        module.fail_json(
            msg="Insertafter and insertbefore parameters are valid only with env=yes"
        )

    if reboot:
        special_time = "reboot"

    # if requested make a backup before making a change
    if backup and not module.check_mode:
        (backuph, backup_file) = tempfile.mkstemp(prefix="crontab")
        crontab.write(backup_file)

    if crontab.cron_file and not do_install:
        if module._diff:
            diff["after"] = ""
            diff["after_header"] = "/dev/null"
        else:
            diff = dict()
        if module.check_mode:
            changed = os.path.isfile(crontab.cron_file)
        else:
            changed = crontab.remove_job_file()
        module.exit_json(changed=changed, cron_file=cron_file, state=state, diff=diff)

    if env:
        if " " in name:
            module.fail_json(msg="Invalid name for environment variable")
        decl = '%s="%s"' % (name, job)
        old_decl = crontab.find_env(name)

        if do_install:
            if len(old_decl) == 0:
                crontab.add_env(decl, insertafter, insertbefore)
                changed = True
            if len(old_decl) > 0 and old_decl[1] != decl:
                crontab.update_env(name, decl)
                changed = True
        else:
            if len(old_decl) > 0:
                crontab.remove_env(name)
                changed = True
    else:
        if do_install:
            for char in ["\r", "\n"]:
                if char in job.strip("\r\n"):
                    warnings.append("Job should not contain line breaks")
                    break

            job = crontab.get_cron_job(
                minute, hour, day, month, weekday, job, special_time, disabled
            )
            old_job = crontab.find_job(name, job)

            if len(old_job) == 0:
                crontab.add_job(name, job)
                changed = True
            if len(old_job) > 0 and old_job[1] != job:
                crontab.update_job(name, job)
                changed = True
            if len(old_job) > 2:
                crontab.update_job(name, job)
                changed = True
        else:
            old_job = crontab.find_job(name)

            if len(old_job) > 0:
                crontab.remove_job(name)
                changed = True

    # no changes to env/job, but existing crontab needs a terminating newline
    if not changed and crontab.n_existing != "":
        if not (crontab.n_existing.endswith("\r") or crontab.n_existing.endswith("\n")):
            changed = True

    res_args = dict(
        jobs=crontab.get_jobnames(),
        envs=crontab.get_envnames(),
        warnings=warnings,
        changed=changed,
    )

    if changed:
        if not module.check_mode:
            crontab.write()
        if module._diff:
            diff["after"] = crontab.render()
            if crontab.cron_file:
                diff["after_header"] = crontab.cron_file
            else:
                if crontab.user:
                    diff["after_header"] = 'crontab for user "%s"' % crontab.user
                else:
                    diff["after_header"] = "crontab"

            res_args["diff"] = diff

    # retain the backup only if crontab or cron file have changed
    if backup and not module.check_mode:
        if changed:
            res_args["backup_file"] = backup_file
        else:
            os.unlink(backup_file)

    if cron_file:
        res_args["cron_file"] = cron_file

    module.exit_json(**res_args)

    # --- should never get here
    module.exit_json(msg="Unable to execute cron task.")