Repository: ansible/ansible

Issue:
interfaces_file module crash in py3: 'TypeError: a bytes-like object is required, not 'str'
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
interfaces_file

##### ANSIBLE VERSION
```sh
$ ansible --version
ansible 2.4.3.0
  config file = /tmp/playbook/ansible.cfg
  configured module search path = [u'/home/alu/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/dist-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.14 (default, Sep 23 2017, 22:06:14) [GCC 7.2.0]
```

##### CONFIGURATION
```sh
$ ansible-config dump --only-changed
DEFAULT_HOST_LIST(/tmp/playbook/ansible.cfg) = [u'/tmp/playbook/hosts.yml']
```

##### OS / ENVIRONMENT
```sh
$ uname -a
Linux local 4.13.0-36-generic #40-Ubuntu SMP Fri Feb 16 20:07:48 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
```

##### SUMMARY
Python3 version of interface_file module would crash when writes the file, as it expects bytes, not string. Python 2 version works fine.

##### STEPS TO REPRODUCE
<!--- Paste example playbooks or commands between quotes below -->
```yaml
- hosts: localhost
  gather_facts: no
  vars:
    ansible_python_interpreter: /usr/bin/python3
  tasks:
    - name: Create interfaces
      copy:
        dest: /tmp/interfaces
        content: |
          iface test_if inet dhcp
    - name: Configure interfaces
      interfaces_file:
        dest: /tmp/interfaces
        iface: test_if
        option: test_key
        value: test_value
```

##### EXPECTED RESULTS
Passed task, just like in py2 version.

##### ACTUAL RESULTS
```sh
$ ansible-playbook playbook_tester.yml -vvv
ansible-playbook 2.4.3.0
  config file = /tmp/playbook/ansible.cfg
  configured module search path = [u'/home/alu/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/dist-packages/ansible
  executable location = /usr/bin/ansible-playbook
  python version = 2.7.14 (default, Sep 23 2017, 22:06:14) [GCC 7.2.0]
Using /tmp/playbook/ansible.cfg as config file
Parsed /tmp/playbook/hosts.yml inventory source with yaml plugin

PLAYBOOK: playbook_tester.yml ***************************************************************************************************************************************************************************************************
1 plays in playbook_tester.yml

PLAY [localhost] ****************************************************************************************************************************************************************************************************************
META: ran handlers

TASK [Create interfaces] ********************************************************************************************************************************************************************************************************
task path: /tmp/playbook/playbook_tester.yml:25
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: alu
<127.0.0.1> EXEC /bin/sh -c 'echo ~ && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293 `" && echo ansible-tmp-1521011146.32-204458885842293="` echo /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293 `" ) && sleep 0'
Using module file /usr/lib/python2.7/dist-packages/ansible/modules/files/stat.py
<127.0.0.1> PUT /tmp/tmpMM0Nsx TO /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293/stat.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293/ /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293/stat.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python3 /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293/stat.py && sleep 0'
Using module file /usr/lib/python2.7/dist-packages/ansible/modules/files/file.py
<127.0.0.1> PUT /tmp/tmpgZOek2 TO /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293/file.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293/ /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293/file.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python3 /home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293/file.py; rm -rf "/home/alu/.ansible/tmp/ansible-tmp-1521011146.32-204458885842293/" > /dev/null 2>&1 && sleep 0'
ok: [localhost] => {
    "changed": false, 
    "checksum": "4453c0fd7661905a389d06e92a399529a90fad82", 
    "diff": {
        "after": {
            "path": "/tmp/interfaces"
        }, 
        "before": {
            "path": "/tmp/interfaces"
        }
    }, 
    "gid": 1000, 
    "group": "alu", 
    "invocation": {
        "module_args": {
            "attributes": null, 
            "backup": null, 
            "content": null, 
            "delimiter": null, 
            "dest": "/tmp/interfaces", 
            "diff_peek": null, 
            "directory_mode": null, 
            "follow": false, 
            "force": false, 
            "group": null, 
            "mode": null, 
            "original_basename": "tmp_4Pbou", 
            "owner": null, 
            "path": "/tmp/interfaces", 
            "recurse": false, 
            "regexp": null, 
            "remote_src": null, 
            "selevel": null, 
            "serole": null, 
            "setype": null, 
            "seuser": null, 
            "src": "tmp_4Pbou", 
            "state": "file", 
            "unsafe_writes": null, 
            "validate": null
        }
    }, 
    "mode": "0664", 
    "owner": "alu", 
    "path": "/tmp/interfaces", 
    "size": 24, 
    "state": "file", 
    "uid": 1000
}

TASK [Configure interfaces] *****************************************************************************************************************************************************************************************************
task path: /tmp/playbook/playbook_tester.yml:30
Using module file /usr/lib/python2.7/dist-packages/ansible/modules/system/interfaces_file.py
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: alu
<127.0.0.1> EXEC /bin/sh -c 'echo ~ && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/alu/.ansible/tmp/ansible-tmp-1521011146.9-234958119086810 `" && echo ansible-tmp-1521011146.9-234958119086810="` echo /home/alu/.ansible/tmp/ansible-tmp-1521011146.9-234958119086810 `" ) && sleep 0'
<127.0.0.1> PUT /tmp/tmpuX7hJp TO /home/alu/.ansible/tmp/ansible-tmp-1521011146.9-234958119086810/interfaces_file.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /home/alu/.ansible/tmp/ansible-tmp-1521011146.9-234958119086810/ /home/alu/.ansible/tmp/ansible-tmp-1521011146.9-234958119086810/interfaces_file.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python3 /home/alu/.ansible/tmp/ansible-tmp-1521011146.9-234958119086810/interfaces_file.py; rm -rf "/home/alu/.ansible/tmp/ansible-tmp-1521011146.9-234958119086810/" > /dev/null 2>&1 && sleep 0'
The full traceback is:
Traceback (most recent call last):
  File "/tmp/ansible_458o94kp/ansible_module_interfaces_file.py", line 385, in <module>
    main()
  File "/tmp/ansible_458o94kp/ansible_module_interfaces_file.py", line 379, in main
    write_changes(module, [d['line'] for d in lines if 'line' in d], dest)
  File "/tmp/ansible_458o94kp/ansible_module_interfaces_file.py", line 333, in write_changes
    f.writelines(lines)
TypeError: a bytes-like object is required, not 'str'

fatal: [localhost]: FAILED! => {
    "changed": false, 
    "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_458o94kp/ansible_module_interfaces_file.py\", line 385, in <module>\n    main()\n  File \"/tmp/ansible_458o94kp/ansible_module_interfaces_file.py\", line 379, in main\n    write_changes(module, [d['line'] for d in lines if 'line' in d], dest)\n  File \"/tmp/ansible_458o94kp/ansible_module_interfaces_file.py\", line 333, in write_changes\n    f.writelines(lines)\nTypeError: a bytes-like object is required, not 'str'\n", 
    "module_stdout": "", 
    "msg": "MODULE FAILURE", 
    "rc": 0
}
	to retry, use: --limit @/tmp/playbook/playbook_tester.retry

PLAY RECAP **********************************************************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1   
```



Discussion:
Files identified in the description:
* [lib/ansible/modules/system/interfaces_file.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/interfaces_file.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @hryamzik
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@alu can you try to change `f = os.fdopen(tmpfd, 'wb')` to `f = os.fdopen(tmpfd, 'w')` [here](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/interfaces_file.py#L332)?
@hryamzik 

Tried that both py3 and py2 then works fine in my case.
Cool, it would be great if you could create a PR for this.

Resolving patch:
diff --git a/lib/ansible/modules/system/interfaces_file.py b/lib/ansible/modules/system/interfaces_file.py
--- a/lib/ansible/modules/system/interfaces_file.py
+++ b/lib/ansible/modules/system/interfaces_file.py
@@ -141,6 +141,7 @@
 import tempfile
 
 from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_bytes
 
 
 def lineDict(line):
@@ -321,7 +322,7 @@ def write_changes(module, lines, dest):
 
     tmpfd, tmpfile = tempfile.mkstemp()
     f = os.fdopen(tmpfd, 'wb')
-    f.writelines(lines)
+    f.writelines(to_bytes(lines, errors='surrogate_or_strict'))
     f.close()
     module.atomic_move(tmpfile, os.path.realpath(dest))
 


Test patch: