Repository: ansible/ansible

Issue:
ini: Lock file when saving to prevent corruption
<!---
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
 - Bug Report / Feature Idea

##### COMPONENT NAME
ini_file

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.2.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/something/.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.6 (default, Nov 23 2017, 15:49:48) [GCC 4.8.4]
```

##### CONFIGURATION
DEFAULT_REMOTE_PORT(/etc/ansible/ansible.cfg) = 22
PERSISTENT_CONNECT_TIMEOUT(/etc/ansible/ansible.cfg) = 30

##### OS / ENVIRONMENT
Ubuntu Server 16.04 LTS, 64-bit

##### SUMMARY
When writing INI files from two instances to the same file and host it sometimes corrupts the file, resulting in wrong INI syntax (duplicates or a half section written to file).

##### STEPS TO REPRODUCE
Try to setup a test host and write to a INI file, execute the playbook/script few times at the same time.

##### EXPECTED RESULTS
I understand that it's a bad concept of potentially having the INI write at the same time, but with some deployment setup it could happen at any time you have scheduled tasks and manual tasks at the same time. It would be awesome to have a lock mechanism to the INI function.

##### ACTUAL RESULTS
Duplicates or weird syntax in the ini file.



Discussion:
Files identified in the description:
* [lib/ansible/plugins/inventory/ini.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/inventory/ini.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 --->
Files identified in the description:
* [lib/ansible/modules/files/ini_file.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/files/ini_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 @jpmens @noseka1
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
> from two instances to the same file and host

Honestly?

It's an exceptional situation that occurred in one time when deploying at the same time and this was due to scheduled rollout and a manual action at the same time to the same host. The file is used to read out the configuration of the deployment by specific server tools (on the server itself) so it needs to be updated several times during deployment and different roles.
I observe that ini_file module opens the file directly to write changes:

 https://github.com/ansible/ansible/blob/eb162bdf7f0b29ef0cc29bbb20c0fa871519910e/lib/ansible/modules/files/ini_file.py#L242 

From paragraph "Common Pitfalls" of Ansible module best practice [link](http://docs.ansible.com/ansible/latest/dev_guide/developing_modules_best_practices.html) I read 

> "Don’t write to files directly; use a temporary file and then use the atomic_move function from ansible.module_utils.basic to move the updated temporary file into place. This prevents data corruption and ensures that the correct context for the file is kept." 

So I think that the "write" section of ini_file module can be modified to use atomic_move function.

I can work on it on my spare time

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
@@ -106,6 +106,8 @@
 
 import os
 import re
+import tempfile
+import traceback
 
 from ansible.module_utils.basic import AnsibleModule
 
@@ -239,11 +241,20 @@ def do_ini(module, filename, section=None, option=None, value=None,
     if changed and not module.check_mode:
         if backup:
             backup_file = module.backup_local(filename)
-        ini_file = open(filename, 'w')
+
         try:
-            ini_file.writelines(ini_lines)
-        finally:
-            ini_file.close()
+            tmpfd, tmpfile = tempfile.mkstemp(dir=module.tmpdir)
+            f = os.fdopen(tmpfd, 'w')
+            f.writelines(ini_lines)
+            f.close()
+        except IOError:
+            module.fail_json(msg="Unable to create temporary file %s", traceback=traceback.format_exc())
+
+        try:
+            module.atomic_move(tmpfile, filename)
+        except IOError:
+            module.ansible.fail_json(msg='Unable to move temporary \
+                                   file %s to %s, IOError' % (tmpfile, filename), traceback=traceback.format_exc())
 
     return (changed, backup_file, diff, msg)
 


Test patch: