Repository: ansible/ansible

Issue:
module: ini_file does not take care of file ending without a newline
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report


##### COMPONENT NAME
ini_file

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.0.2.0
  config file =
  configured module search path = Default w/o overrides
```
```
ansible 2.2.0.0
  config file =
  configured module search path = Default w/o overrides
```

##### CONFIGURATION
None

##### OS / ENVIRONMENT
Managing: Linux 4.4.6-ti-r15 #1 SMP Tue Apr 5 08:05:26 UTC 2016 armv7l GNU/Linux
Running: Linux 4.4.6-ti-r15 #1 SMP Tue Apr 5 08:05:26 UTC 2016 armv7l GNU/Linux

##### SUMMARY
I've got a config file which had no trailing newline and the ini_file didnt took care of it and created an 'invalid' ini file.

##### STEPS TO REPRODUCE

Create a config similar to mine:
```
printf "[main]\nOPTION=value" | tee /tmp/some_file.ini | hexdump -C
00000000  5b 6d 61 69 6e 5d 0a 4f  50 54 49 4f 4e 3d 76 61  |[main].OPTION=va|
00000010  6c 75 65                                          |lue|
00000013
```

Issue ansible task:
```
ansible all -i "localhost," -c local -m ini_file -a "dest=/tmp/some_file.ini section=main option=OPTION2 value=TEST"
```

##### EXPECTED RESULTS
```
hexdump -C /tmp/some_file.ini
00000000  5b 6d 61 69 6e 5d 0a 4f  50 54 49 4f 4e 3d 76 61  |[main].OPTION=va|
00000010  6c 75 65 0a 4f 50 54 49  4f 4e 32 20 3d 20 54 45  |lue.OPTION2 = TE|
00000020  53 54 0a                                          |ST.|
00000023

cat /tmp/some_file.ini
[main]
OPTION=value
OPTION2 = TEST
```

##### ACTUAL RESULTS
```
hexdump -C /tmp/some_file.ini
00000000  5b 6d 61 69 6e 5d 0a 4f  50 54 49 4f 4e 3d 76 61  |[main].OPTION=va|
00000010  6c 75 65 4f 50 54 49 4f  4e 32 20 3d 20 54 45 53  |lueOPTION2 = TES|
00000020  54 0a                                             |T.|
00000022

cat /tmp/some_file.ini
[main]
OPTION=valueOPTION2 = TEST
```


Discussion:
cc @jpmens, @noseka1
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Same issue on 
```
ansible 2.2.0.0
  config file =
  configured module search path = Default w/o overrides
```
needs_contributor
I'll pick this up if that's OK.
please do!


Resolving patch:
diff --git a/lib/ansible/modules/files/ini_file.py b/lib/ansible/modules/files/ini_file.py
--- a/lib/ansible/modules/files/ini_file.py
+++ b/lib/ansible/modules/files/ini_file.py
@@ -34,6 +34,8 @@
        to manage the file as a whole with, say, M(template) or M(assemble). Adds missing
        sections if they don't exist.
      - Before version 2.0, comments are discarded when the source file is read, and therefore will not show up in the destination file.
+     - Since version 2.3, this module adds missing ending newlines to files to keep in line with the POSIX standard, even when
+       no modifications need to be applied.
 version_added: "0.9"
 options:
   dest:
@@ -164,12 +166,18 @@ def do_ini(module, filename, section=None, option=None, value=None,
     if module._diff:
         diff['before'] = ''.join(ini_lines)
 
+    changed = False
+
+    # last line of file may not contain a trailing newline
+    if ini_lines[-1] == "" or ini_lines[-1][-1] != '\n':
+        ini_lines[-1] += '\n'
+        changed = True
+
     # append a fake section line to simplify the logic
     ini_lines.append('[')
 
     within_section = not section
     section_start = 0
-    changed = False
     msg = 'OK'
     if no_extra_spaces:
         assignment_format = '%s=%s\n'
@@ -203,11 +211,12 @@ def do_ini(module, filename, section=None, option=None, value=None,
                     # change the existing option line
                     if match_opt(option, line):
                         newline = assignment_format % (option, value)
-                        changed = ini_lines[index] != newline
-                        if changed:
+                        option_changed = ini_lines[index] != newline
+                        changed = changed or option_changed
+                        if option_changed:
                             msg = 'option changed'
                         ini_lines[index] = newline
-                        if changed:
+                        if option_changed:
                             # remove all possible option occurrences from the rest of the section
                             index = index + 1
                             while index < len(ini_lines):


Test patch: