# Buggy code
def enforce_state(module, params):
    """
    Add or remove key.
    """

    user = params["user"]
    key = params["key"]
    path = params.get("path", None)
    manage_dir = params.get("manage_dir", True)
    state = params.get("state", "present")
    key_options = params.get("key_options", None)
    exclusive = params.get("exclusive", False)
    error_msg = "Error getting key from: %s"

    # if the key is a url, request it and use it as key source
    if key.startswith("http"):
        try:
            resp, info = fetch_url(module, key)
            if info["status"] != 200:
                module.fail_json(msg=error_msg % key)
            else:
                key = resp.read()
        except Exception:
            module.fail_json(msg=error_msg % key)

    # extract individual keys into an array, skipping blank lines and comments
    new_keys = [s for s in key.splitlines() if s and not s.startswith("#")]

    # check current state -- just get the filename, don't create file
    do_write = False
    params["keyfile"] = keyfile(module, user, do_write, path, manage_dir)
    existing_content = readfile(params["keyfile"])
    existing_keys = parsekeys(module, existing_content)

    # Add a place holder for keys that should exist in the state=present and
    # exclusive=true case
    keys_to_exist = []

    # we will order any non exclusive new keys higher than all the existing keys,
    # resulting in the new keys being written to the key file after existing keys, but
    # in the order of new_keys
    max_rank_of_existing_keys = len(existing_keys)

    # Check our new keys, if any of them exist we'll continue.
    for rank_index, new_key in enumerate(new_keys):
        parsed_new_key = parsekey(module, new_key, rank=rank_index)

        if not parsed_new_key:
            module.fail_json(msg="invalid key specified: %s" % new_key)

        if key_options is not None:
            parsed_options = parseoptions(module, key_options)
            # rank here is the rank in the provided new keys, which may be unrelated to rank in existing_keys
            parsed_new_key = (
                parsed_new_key[0],
                parsed_new_key[1],
                parsed_options,
                parsed_new_key[3],
                parsed_new_key[4],
            )

        matched = False
        non_matching_keys = []

        if parsed_new_key[0] in existing_keys:
            # Then we check if everything (except the rank at index 4) matches, including
            # the key type and options. If not, we append this
            # existing key to the non-matching list
            # We only want it to match everything when the state
            # is present
            if (
                parsed_new_key[:4] != existing_keys[parsed_new_key[0]][:4]
                and state == "present"
            ):
                non_matching_keys.append(existing_keys[parsed_new_key[0]])
            else:
                matched = True

        # handle idempotent state=present
        if state == "present":
            keys_to_exist.append(parsed_new_key[0])
            if len(non_matching_keys) > 0:
                for non_matching_key in non_matching_keys:
                    if non_matching_key[0] in existing_keys:
                        del existing_keys[non_matching_key[0]]
                        do_write = True

            # new key that didn't exist before. Where should it go in the ordering?
            if not matched:
                # We want the new key to be after existing keys if not exclusive (rank > max_rank_of_existing_keys)
                total_rank = max_rank_of_existing_keys + parsed_new_key[4]
                # replace existing key tuple with new parsed key with its total rank
                existing_keys[parsed_new_key[0]] = (
                    parsed_new_key[0],
                    parsed_new_key[1],
                    parsed_new_key[2],
                    parsed_new_key[3],
                    total_rank,
                )
                do_write = True

        elif state == "absent":
            if not matched:
                continue
            del existing_keys[parsed_new_key[0]]
            do_write = True

    # remove all other keys to honor exclusive
    # for 'exclusive', make sure keys are written in the order the new keys were
    if state == "present" and exclusive:
        to_remove = frozenset(existing_keys).difference(keys_to_exist)
        for key in to_remove:
            del existing_keys[key]
            do_write = True

    if do_write:
        filename = keyfile(module, user, do_write, path, manage_dir)
        new_content = serialize(existing_keys)
        diff = {
            "before_header": params["keyfile"],
            "after_header": filename,
            "before": existing_content,
            "after": new_content,
        }
        if module.check_mode:
            module.exit_json(changed=True, diff=diff)
        writefile(module, filename, new_content)
        params["changed"] = True
        params["diff"] = diff
    else:
        if module.check_mode:
            module.exit_json(changed=False)

    return params


# Traceback (nTypeError)
(venv) james@codebox:~/active/boss$ ansible-playbook test.yml -vvvv
Using /home/james/active/boss/ansible.cfg as config file
Loading callback plugin default of type stdout, v2.0 from /home/james/active/boss/venv/lib/python3.5/site-packages/ansible/plugins/callback/__init__.py

PLAYBOOK: test.yml *************************************************************
1 plays in test.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
Using module file /home/james/active/boss/venv/lib/python3.5/site-packages/ansible/modules/core/system/setup.py
<104.131.17.109> ESTABLISH SSH CONNECTION FOR USER: root
<104.131.17.109> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/james/.ansible/cp/ansible-ssh-%h-%p-%r 104.131.17.109 '/bin/sh -c '"'"'( umask 77 &amp;&amp; mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1483808135.2190897-90788529272429 `" &amp;&amp; echo ansible-tmp-1483808135.2190897-90788529272429="` echo $HOME/.ansible/tmp/ansible-tmp-1483808135.2190897-90788529272429 `" ) &amp;&amp; sleep 0'"'"''
<104.131.17.109> PUT /tmp/tmpqi9xuich TO /root/.ansible/tmp/ansible-tmp-1483808135.2190897-90788529272429/setup.py
<104.131.17.109> SSH: EXEC sftp -b - -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/james/.ansible/cp/ansible-ssh-%h-%p-%r '[104.131.17.109]'
<104.131.17.109> ESTABLISH SSH CONNECTION FOR USER: root
<104.131.17.109> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/james/.ansible/cp/ansible-ssh-%h-%p-%r 104.131.17.109 '/bin/sh -c '"'"'chmod u+x /root/.ansible/tmp/ansible-tmp-1483808135.2190897-90788529272429/ /root/.ansible/tmp/ansible-tmp-1483808135.2190897-90788529272429/setup.py &amp;&amp; sleep 0'"'"''
<104.131.17.109> ESTABLISH SSH CONNECTION FOR USER: root
<104.131.17.109> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/james/.ansible/cp/ansible-ssh-%h-%p-%r -tt 104.131.17.109 '/bin/sh -c '"'"'/usr/bin/python3.5 /root/.ansible/tmp/ansible-tmp-1483808135.2190897-90788529272429/setup.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1483808135.2190897-90788529272429/" > /dev/null 2>&amp;1 &amp;&amp; sleep 0'"'"''
ok: [104.131.17.109]

TASK [authorized_key] **********************************************************
task path: /home/james/active/boss/test.yml:6
Using module file /home/james/active/boss/venv/lib/python3.5/site-packages/ansible/modules/core/system/authorized_key.py
<104.131.17.109> ESTABLISH SSH CONNECTION FOR USER: root
<104.131.17.109> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/james/.ansible/cp/ansible-ssh-%h-%p-%r 104.131.17.109 '/bin/sh -c '"'"'( umask 77 &amp;&amp; mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1483808139.0550506-81997757222281 `" &amp;&amp; echo ansible-tmp-1483808139.0550506-81997757222281="` echo $HOME/.ansible/tmp/ansible-tmp-1483808139.0550506-81997757222281 `" ) &amp;&amp; sleep 0'"'"''
<104.131.17.109> PUT /tmp/tmpinuhh2o4 TO /root/.ansible/tmp/ansible-tmp-1483808139.0550506-81997757222281/authorized_key.py
<104.131.17.109> SSH: EXEC sftp -b - -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/james/.ansible/cp/ansible-ssh-%h-%p-%r '[104.131.17.109]'
<104.131.17.109> ESTABLISH SSH CONNECTION FOR USER: root
<104.131.17.109> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/james/.ansible/cp/ansible-ssh-%h-%p-%r 104.131.17.109 '/bin/sh -c '"'"'chmod u+x /root/.ansible/tmp/ansible-tmp-1483808139.0550506-81997757222281/ /root/.ansible/tmp/ansible-tmp-1483808139.0550506-81997757222281/authorized_key.py &amp;&amp; sleep 0'"'"''
<104.131.17.109> ESTABLISH SSH CONNECTION FOR USER: root
<104.131.17.109> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=root -o ConnectTimeout=10 -o ControlPath=/home/james/.ansible/cp/ansible-ssh-%h-%p-%r -tt 104.131.17.109 '/bin/sh -c '"'"'/usr/bin/python3.5 /root/.ansible/tmp/ansible-tmp-1483808139.0550506-81997757222281/authorized_key.py; rm -rf "/root/.ansible/tmp/ansible-tmp-1483808139.0550506-81997757222281/" > /dev/null 2>&amp;1 &amp;&amp; sleep 0'"'"''
fatal: [104.131.17.109]: FAILED! => {
"changed": false,
"failed": true,
"invocation": {
"module_name": "authorized_key"
},
"module_stderr": "OpenSSH_6.9p1 Ubuntu-2ubuntu0.2, OpenSSL 1.0.2d 9 Jul 2015\r\ndebug1: Reading configuration data /home/james/.ssh/config\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 = 22341\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 104.131.17.109 closed.\r\n",
"module_stdout": "Traceback (most recent call last):\r\n  File \"/tmp/ansible_grkuvdsh/ansible_module_authorized_key.py\", line 527, in <module>\r\n    main()\r\n  File \"/tmp/ansible_grkuvdsh/ansible_module_authorized_key.py\", line 523, in main\r\n    results = enforce_state(module, module.params)\r\n  File \"/tmp/ansible_grkuvdsh/ansible_module_authorized_key.py\", line 433, in enforce_state\r\n    key = [s for s in key.splitlines() if s and not s.startswith('#')]\r\n  File \"/tmp/ansible_grkuvdsh/ansible_module_authorized_key.py\", line 433, in <listcomp>\r\n    key = [s for s in key.splitlines() if s and not s.startswith('#')]\r\nTypeError: startswith first arg must be bytes or a tuple of bytes, not str\r\n",
"msg": "MODULE FAILURE"
}
to retry, use: --limit @/home/james/active/boss/test.retry

PLAY RECAP *********************************************************************
104.131.17.109             : ok=1    changed=0    unreachable=0    failed=1

# Fixed code
def enforce_state(module, params):
    """
    Add or remove key.
    """

    user = params["user"]
    key = params["key"]
    path = params.get("path", None)
    manage_dir = params.get("manage_dir", True)
    state = params.get("state", "present")
    key_options = params.get("key_options", None)
    exclusive = params.get("exclusive", False)
    error_msg = "Error getting key from: %s"

    # if the key is a url, request it and use it as key source
    if key.startswith("http"):
        try:
            resp, info = fetch_url(module, key)
            if info["status"] != 200:
                module.fail_json(msg=error_msg % key)
            else:
                key = resp.read()
        except Exception:
            module.fail_json(msg=error_msg % key)

        # resp.read gives bytes on python3, convert to native string type
        key = to_native(key, errors="surrogate_or_strict")

    # extract individual keys into an array, skipping blank lines and comments
    new_keys = [s for s in key.splitlines() if s and not s.startswith("#")]

    # check current state -- just get the filename, don't create file
    do_write = False
    params["keyfile"] = keyfile(module, user, do_write, path, manage_dir)
    existing_content = readfile(params["keyfile"])
    existing_keys = parsekeys(module, existing_content)

    # Add a place holder for keys that should exist in the state=present and
    # exclusive=true case
    keys_to_exist = []

    # we will order any non exclusive new keys higher than all the existing keys,
    # resulting in the new keys being written to the key file after existing keys, but
    # in the order of new_keys
    max_rank_of_existing_keys = len(existing_keys)

    # Check our new keys, if any of them exist we'll continue.
    for rank_index, new_key in enumerate(new_keys):
        parsed_new_key = parsekey(module, new_key, rank=rank_index)

        if not parsed_new_key:
            module.fail_json(msg="invalid key specified: %s" % new_key)

        if key_options is not None:
            parsed_options = parseoptions(module, key_options)
            # rank here is the rank in the provided new keys, which may be unrelated to rank in existing_keys
            parsed_new_key = (
                parsed_new_key[0],
                parsed_new_key[1],
                parsed_options,
                parsed_new_key[3],
                parsed_new_key[4],
            )

        matched = False
        non_matching_keys = []

        if parsed_new_key[0] in existing_keys:
            # Then we check if everything (except the rank at index 4) matches, including
            # the key type and options. If not, we append this
            # existing key to the non-matching list
            # We only want it to match everything when the state
            # is present
            if (
                parsed_new_key[:4] != existing_keys[parsed_new_key[0]][:4]
                and state == "present"
            ):
                non_matching_keys.append(existing_keys[parsed_new_key[0]])
            else:
                matched = True

        # handle idempotent state=present
        if state == "present":
            keys_to_exist.append(parsed_new_key[0])
            if len(non_matching_keys) > 0:
                for non_matching_key in non_matching_keys:
                    if non_matching_key[0] in existing_keys:
                        del existing_keys[non_matching_key[0]]
                        do_write = True

            # new key that didn't exist before. Where should it go in the ordering?
            if not matched:
                # We want the new key to be after existing keys if not exclusive (rank > max_rank_of_existing_keys)
                total_rank = max_rank_of_existing_keys + parsed_new_key[4]
                # replace existing key tuple with new parsed key with its total rank
                existing_keys[parsed_new_key[0]] = (
                    parsed_new_key[0],
                    parsed_new_key[1],
                    parsed_new_key[2],
                    parsed_new_key[3],
                    total_rank,
                )
                do_write = True

        elif state == "absent":
            if not matched:
                continue
            del existing_keys[parsed_new_key[0]]
            do_write = True

    # remove all other keys to honor exclusive
    # for 'exclusive', make sure keys are written in the order the new keys were
    if state == "present" and exclusive:
        to_remove = frozenset(existing_keys).difference(keys_to_exist)
        for key in to_remove:
            del existing_keys[key]
            do_write = True

    if do_write:
        filename = keyfile(module, user, do_write, path, manage_dir)
        new_content = serialize(existing_keys)
        diff = {
            "before_header": params["keyfile"],
            "after_header": filename,
            "before": existing_content,
            "after": new_content,
        }
        if module.check_mode:
            module.exit_json(changed=True, diff=diff)
        writefile(module, filename, new_content)
        params["changed"] = True
        params["diff"] = diff
    else:
        if module.check_mode:
            module.exit_json(changed=False)

    return params