# Buggy code
def main():
    pam_items = [
        "core",
        "data",
        "fsize",
        "memlock",
        "nofile",
        "rss",
        "stack",
        "cpu",
        "nproc",
        "as",
        "maxlogins",
        "maxsyslogins",
        "priority",
        "locks",
        "sigpending",
        "msgqueue",
        "nice",
        "rtprio",
        "chroot",
    ]

    pam_types = ["soft", "hard", "-"]

    limits_conf = "/etc/security/limits.conf"

    module = AnsibleModule(
        # not checking because of daisy chain to file module
        argument_spec=dict(
            domain=dict(required=True, type="str"),
            limit_type=dict(required=True, type="str", choices=pam_types),
            limit_item=dict(required=True, type="str", choices=pam_items),
            value=dict(required=True, type="str"),
            use_max=dict(default=False, type="bool"),
            use_min=dict(default=False, type="bool"),
            backup=dict(default=False, type="bool"),
            dest=dict(default=limits_conf, type="str"),
            comment=dict(required=False, default="", type="str"),
        )
    )

    domain = module.params["domain"]
    limit_type = module.params["limit_type"]
    limit_item = module.params["limit_item"]
    value = module.params["value"]
    use_max = module.params["use_max"]
    use_min = module.params["use_min"]
    backup = module.params["backup"]
    limits_conf = module.params["dest"]
    new_comment = module.params["comment"]

    changed = False

    if os.path.isfile(limits_conf):
        if not os.access(limits_conf, os.W_OK):
            module.fail_json(msg="%s is not writable. Use sudo" % (limits_conf))
    else:
        module.fail_json(
            msg="%s is not visible (check presence, access rights, use sudo)"
            % (limits_conf)
        )

    if use_max and use_min:
        module.fail_json(msg="Cannot use use_min and use_max at the same time.")

    if not (value in ["unlimited", "infinity", "-1"] or value.isdigit()):
        module.fail_json(
            msg="Argument 'value' can be one of 'unlimited', 'infinity', '-1' or positive number. Refer to manual pages for more details."
        )

    # Backup
    if backup:
        backup_file = module.backup_local(limits_conf)

    space_pattern = re.compile(r"\s+")

    message = ""
    f = open(limits_conf, "r")
    # Tempfile
    nf = tempfile.NamedTemporaryFile()

    found = False
    new_value = value

    for line in f:
        if line.startswith("#"):
            nf.write(line)
            continue

        newline = re.sub(space_pattern, " ", line).strip()
        if not newline:
            nf.write(line)
            continue

        # Remove comment in line
        newline = newline.split("#", 1)[0]
        try:
            old_comment = line.split("#", 1)[1]
        except:
            old_comment = ""

        newline = newline.rstrip()

        if not new_comment:
            new_comment = old_comment

        if new_comment:
            new_comment = "\t#" + new_comment

        line_fields = newline.split(" ")

        if len(line_fields) != 4:
            nf.write(line)
            continue

        line_domain = line_fields[0]
        line_type = line_fields[1]
        line_item = line_fields[2]
        actual_value = line_fields[3]

        if not (
            actual_value in ["unlimited", "infinity", "-1"] or actual_value.isdigit()
        ):
            module.fail_json(
                msg="Invalid configuration of '%s'. Current value of %s is unsupported."
                % (limits_conf, line_item)
            )

        # Found the line
        if (
            line_domain == domain
            and line_type == limit_type
            and line_item == limit_item
        ):
            found = True
            if value == actual_value:
                message = line
                nf.write(line)
                continue

            actual_value_unlimited = actual_value in ["unlimited", "infinity", "-1"]
            value_unlimited = value in ["unlimited", "infinity", "-1"]

            if use_max:
                if value.isdigit() and actual_value.isdigit():
                    new_value = str(max(int(value), int(actual_value)))
                elif actual_value_unlimited:
                    new_value = actual_value
                else:
                    new_value = value

            if use_min:
                if value.isdigit() and actual_value.isdigit():
                    new_value = str(min(int(value), int(actual_value)))
                elif value_unlimited:
                    new_value = actual_value
                else:
                    new_value = value

            # Change line only if value has changed
            if new_value != actual_value:
                changed = True
                new_limit = (
                    domain
                    + "\t"
                    + limit_type
                    + "\t"
                    + limit_item
                    + "\t"
                    + new_value
                    + new_comment
                    + "\n"
                )
                message = new_limit
                nf.write(new_limit)
            else:
                message = line
                nf.write(line)
        else:
            nf.write(line)

    if not found:
        changed = True
        new_limit = (
            domain
            + "\t"
            + limit_type
            + "\t"
            + limit_item
            + "\t"
            + new_value
            + new_comment
            + "\n"
        )
        message = new_limit
        nf.write(new_limit)

    f.close()
    nf.flush()

    # Copy tempfile to newfile
    module.atomic_move(nf.name, f.name)

    try:
        nf.close()
    except:
        pass

    res_args = dict(changed=changed, msg=message)

    if backup:
        res_args["backup_file"] = backup_file

    module.exit_json(**res_args)


# Traceback (nTypeError)
failed: [10.7.42.132] (item={u'domain': u'buildteam', u'limit_item': u'nofile', u'value': u'524288', u'limit_type': u'soft'}) => {
"failed": true,
"item": {
"domain": "buildteam",
"limit_item": "nofile",
"limit_type": "soft",
"value": "524288"
},
"module_stderr": "OpenSSH_7.2p2 Ubuntu-4ubuntu2.1, OpenSSL 1.0.2g  1 Mar 2016\r\ndebug1: Reading configuration data /home/maxsolar/.ssh/config\r\ndebug1: /home/maxsolar/.ssh/config line 9: Applying options for 10.7.42.*\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug1: /etc/ssh/ssh_config line 19: Applying options for *\r\ndebug1: auto-mux: Trying existing master\r\ndebug2: fd 3 setting O_NONBLOCK\r\ndebug2: mux_client_hello_exchange: master version 4\r\ndebug3: mux_client_forwards: request forwardings: 0 local, 0 remote\r\ndebug3: mux_client_request_session: entering\r\ndebug3: mux_client_request_alive: entering\r\ndebug3: mux_client_request_alive: done pid = 8486\r\ndebug3: mux_client_request_session: session request sent\r\ndebug1: mux_client_request_session: master session id: 2\r\ndebug3: mux_client_read_packet: read header failed: Broken pipe\r\ndebug2: Received exit status from master 0\r\nShared connection to 10.7.42.132 closed.\r\n",
"module_stdout": "\r\nTraceback (most recent call last):\r\n  File \"/tmp/ansible_nu6l4jt0/ansible_module_pam_limits.py\", line 292, in <module>\r\n    main()\r\n  File \"/tmp/ansible_nu6l4jt0/ansible_module_pam_limits.py\", line 185, in main\r\n    nf.write(line)\r\n  File \"/usr/lib/python3.5/tempfile.py\", line 622, in func_wrapper\r\n    return func(*args, **kwargs)\r\nTypeError: a bytes-like object is required, not 'str'\r\n",
"msg": "MODULE FAILURE",
"rc": 0
}
failed: [10.7.42.132] (item={u'domain': u'buildteam', u'limit_item': u'nofile', u'value': u'524288', u'limit_type': u'hard'}) => {
"failed": true,
"item": {
"domain": "buildteam",
"limit_item": "nofile",
"limit_type": "hard",
"value": "524288"
},
"module_stderr": "OpenSSH_7.2p2 Ubuntu-4ubuntu2.1, OpenSSL 1.0.2g  1 Mar 2016\r\ndebug1: Reading configuration data /home/maxsolar/.ssh/config\r\ndebug1: /home/maxsolar/.ssh/config line 9: Applying options for 10.7.42.*\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug1: /etc/ssh/ssh_config line 19: Applying options for *\r\ndebug1: auto-mux: Trying existing master\r\ndebug2: fd 3 setting O_NONBLOCK\r\ndebug2: mux_client_hello_exchange: master version 4\r\ndebug3: mux_client_forwards: request forwardings: 0 local, 0 remote\r\ndebug3: mux_client_request_session: entering\r\ndebug3: mux_client_request_alive: entering\r\ndebug3: mux_client_request_alive: done pid = 8486\r\ndebug3: mux_client_request_session: session request sent\r\ndebug1: mux_client_request_session: master session id: 2\r\ndebug3: mux_client_read_packet: read header failed: Broken pipe\r\ndebug2: Received exit status from master 0\r\nShared connection to 10.7.42.132 closed.\r\n",
"module_stdout": "\r\nTraceback (most recent call last):\r\n  File \"/tmp/ansible_di9uwhf9/ansible_module_pam_limits.py\", line 292, in <module>\r\n    main()\r\n  File \"/tmp/ansible_di9uwhf9/ansible_module_pam_limits.py\", line 185, in main\r\n    nf.write(line)\r\n  File \"/usr/lib/python3.5/tempfile.py\", line 622, in func_wrapper\r\n    return func(*args, **kwargs)\r\nTypeError: a bytes-like object is required, not 'str'\r\n",
"msg": "MODULE FAILURE",
"rc": 0
}

# Fixed code
def main():
    pam_items = [
        "core",
        "data",
        "fsize",
        "memlock",
        "nofile",
        "rss",
        "stack",
        "cpu",
        "nproc",
        "as",
        "maxlogins",
        "maxsyslogins",
        "priority",
        "locks",
        "sigpending",
        "msgqueue",
        "nice",
        "rtprio",
        "chroot",
    ]

    pam_types = ["soft", "hard", "-"]

    limits_conf = "/etc/security/limits.conf"

    module = AnsibleModule(
        # not checking because of daisy chain to file module
        argument_spec=dict(
            domain=dict(required=True, type="str"),
            limit_type=dict(required=True, type="str", choices=pam_types),
            limit_item=dict(required=True, type="str", choices=pam_items),
            value=dict(required=True, type="str"),
            use_max=dict(default=False, type="bool"),
            use_min=dict(default=False, type="bool"),
            backup=dict(default=False, type="bool"),
            dest=dict(default=limits_conf, type="str"),
            comment=dict(required=False, default="", type="str"),
        )
    )

    domain = module.params["domain"]
    limit_type = module.params["limit_type"]
    limit_item = module.params["limit_item"]
    value = module.params["value"]
    use_max = module.params["use_max"]
    use_min = module.params["use_min"]
    backup = module.params["backup"]
    limits_conf = module.params["dest"]
    new_comment = module.params["comment"]

    changed = False

    if os.path.isfile(limits_conf):
        if not os.access(limits_conf, os.W_OK):
            module.fail_json(msg="%s is not writable. Use sudo" % (limits_conf))
    else:
        module.fail_json(
            msg="%s is not visible (check presence, access rights, use sudo)"
            % (limits_conf)
        )

    if use_max and use_min:
        module.fail_json(msg="Cannot use use_min and use_max at the same time.")

    if not (value in ["unlimited", "infinity", "-1"] or value.isdigit()):
        module.fail_json(
            msg="Argument 'value' can be one of 'unlimited', 'infinity', '-1' or positive number. Refer to manual pages for more details."
        )

    # Backup
    if backup:
        backup_file = module.backup_local(limits_conf)

    space_pattern = re.compile(r"\s+")

    message = ""
    f = open(limits_conf, "rb")
    # Tempfile
    nf = tempfile.NamedTemporaryFile(mode="w+")

    found = False
    new_value = value

    for line in f:
        line = to_native(line, errors="surrogate_or_strict")
        if line.startswith("#"):
            nf.write(line)
            continue

        newline = re.sub(space_pattern, " ", line).strip()
        if not newline:
            nf.write(line)
            continue

        # Remove comment in line
        newline = newline.split("#", 1)[0]
        try:
            old_comment = line.split("#", 1)[1]
        except:
            old_comment = ""

        newline = newline.rstrip()

        if not new_comment:
            new_comment = old_comment

        if new_comment:
            new_comment = "\t#" + new_comment

        line_fields = newline.split(" ")

        if len(line_fields) != 4:
            nf.write(line)
            continue

        line_domain = line_fields[0]
        line_type = line_fields[1]
        line_item = line_fields[2]
        actual_value = line_fields[3]

        if not (
            actual_value in ["unlimited", "infinity", "-1"] or actual_value.isdigit()
        ):
            module.fail_json(
                msg="Invalid configuration of '%s'. Current value of %s is unsupported."
                % (limits_conf, line_item)
            )

        # Found the line
        if (
            line_domain == domain
            and line_type == limit_type
            and line_item == limit_item
        ):
            found = True
            if value == actual_value:
                message = line
                nf.write(line)
                continue

            actual_value_unlimited = actual_value in ["unlimited", "infinity", "-1"]
            value_unlimited = value in ["unlimited", "infinity", "-1"]

            if use_max:
                if value.isdigit() and actual_value.isdigit():
                    new_value = str(max(int(value), int(actual_value)))
                elif actual_value_unlimited:
                    new_value = actual_value
                else:
                    new_value = value

            if use_min:
                if value.isdigit() and actual_value.isdigit():
                    new_value = str(min(int(value), int(actual_value)))
                elif value_unlimited:
                    new_value = actual_value
                else:
                    new_value = value

            # Change line only if value has changed
            if new_value != actual_value:
                changed = True
                new_limit = (
                    domain
                    + "\t"
                    + limit_type
                    + "\t"
                    + limit_item
                    + "\t"
                    + new_value
                    + new_comment
                    + "\n"
                )
                message = new_limit
                nf.write(new_limit)
            else:
                message = line
                nf.write(line)
        else:
            nf.write(line)

    if not found:
        changed = True
        new_limit = (
            domain
            + "\t"
            + limit_type
            + "\t"
            + limit_item
            + "\t"
            + new_value
            + new_comment
            + "\n"
        )
        message = new_limit
        nf.write(new_limit)

    f.close()
    nf.flush()

    # Copy tempfile to newfile
    module.atomic_move(nf.name, f.name)

    try:
        nf.close()
    except:
        pass

    res_args = dict(changed=changed, msg=message)

    if backup:
        res_args["backup_file"] = backup_file

    module.exit_json(**res_args)