Repository: ansible/ansible-modules-core

Issue:
ini_file fails --check when file doesn't exist
##### Issue Type:
- Bug Report
##### Ansible Version:

$ ansible --version
ansible 1.9.1
  configured module search path = None

$ ansible --version
ansible 2.0.0
  config file = 
  configured module search path = None

$ git log -n 1

```
commit 88e1ef8c9bc6434786ce10dbf0ed46a5b2448c1c
Author: Brian Coca <brian.coca+git@gmail.com>
Date:   Thu Jul 23 20:47:24 2015 -0400

    implemented --step in v2
```
##### Ansible Configuration:

<not used>
##### Environment:

running on:
Darwin xxxxxxxxxx.local 14.4.0 Darwin Kernel Version 14.4.0: Thu May 28 11:35:04 PDT 2015; root:xnu-2782.30.5~1/RELEASE_X86_64 x86_64

managing:
Linux ip-xxxxxxxxx-125 3.14.35-28.38.amzn1.x86_64 #1 SMP Wed Mar 11 22:50:37 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
##### Summary:

`ini_file` module fails `ansible-playbook --check` if file does not exist. This prevents any further checks for that host.
##### Steps To Reproduce:
1. Create a task to add a value to a not-yet-existing file on target
2. Run playbook with `--check` option (dry-run)
3. Run playbook without `--check` option (live-run)

Example of (1):

```
- name: install hgrc
  tags: test
  ini_file:
    dest: ~/.hgrc
    section: extensions
    option: bundleclone
    value: ~/.hgext/bundleclone.py
    state: present
```
##### Expected Results:

I expected both steps (2) & (3) to be successful.
In particular, I expected any selected tasks after (2) to also be evaluated by `--check` mode.
##### Actual Results:

(2) failed, and (3) was successful. Subsequent runs of (2) succeeded (once file existed).

Failure message from ansible v2.0:

```
TASK [install hgrc] *************************************************************
fatal: [xxxxxxxxxxxxx]: FAILED! => {
    "changed": false, 
    "failed": true, 
    "msg": "BECOME-SUCCESS-qbihvwjllqutgqdveipfwrshmgihqpsn\r\nTraceback (most recent call last):\r\n  File \"/tmp/ansible-tmp-1437703308.32-120186831188640/ini_file\", line 1882, in <module>\r\n    main()\r\n  File \"/tmp/ansible-tmp-1437703308.32-120186831188640/ini_file\", line 209, in main\r\n    changed = module.set_fs_attributes_if_different(file_args, changed)\r\n  File \"/tmp/ansible-tmp-1437703308.32-120186831188640/ini_file\", line 1037, in set_fs_attributes_if_different\r\n    file_args['path'], file_args['mode'], changed\r\n  File \"/tmp/ansible-tmp-1437703308.32-120186831188640/ini_file\", line 875, in set_mode_if_different\r\n    path_stat = os.lstat(path)\r\nOSError: [Errno 2] No such file or directory: '/home/vcs2vcs/.hgrc'\r\n", 
    "parsed": false
}

PLAY RECAP **********************************************************************
xxxxxxxxxxxxx              : ok=4    changed=1    unreachable=0    failed=1   
```



Discussion:
<!-- @hwine: Thanks for filing this bug report! -->

@jpmens, @noseka1, ping. This issue is waiting on your response.
[click here for bot help](https://github.com/jctanner/ansibullbot/blob/ISSUE_TRIAGE_CLASS/ISSUE_HELP.md)

<!--- boilerplate: issue_bug_new --->


Resolving patch:
diff --git a/files/ini_file.py b/files/ini_file.py
--- a/files/ini_file.py
+++ b/files/ini_file.py
@@ -77,6 +77,14 @@
      required: false
      default: false
      version_added: "2.1"
+  create:
+     required: false
+     choices: [ "yes", "no" ]
+     default: "no"
+     description:
+       - If specified, the file will be created if it does not already exist.
+         By default it will fail if the file is missing.
+     version_added: "2.2"
 notes:
    - While it is possible to add an I(option) without specifying a I(value), this makes
      no sense.
@@ -123,25 +131,38 @@ def match_active_opt(option, line):
 # ==============================================================
 # do_ini
 
-def do_ini(module, filename, section=None, option=None, value=None, state='present', backup=False, no_extra_spaces=False):
+def do_ini(module, filename, section=None, option=None, value=None,
+        state='present', backup=False, no_extra_spaces=False, create=False):
 
+    diff = {'before': '',
+            'after': '',
+            'before_header': '%s (content)' % filename,
+            'after_header': '%s (content)' % filename}
 
     if not os.path.exists(filename):
-      try:
-        open(filename,'w').close()
-      except:
-        module.fail_json(msg="Destination file %s not writable" % filename)
-    ini_file = open(filename, 'r')
-    try:
-        ini_lines = ini_file.readlines()
-        # append a fake section line to simplify the logic
-        ini_lines.append('[')
-    finally:
-        ini_file.close()
+        if not create:
+            module.fail_json(rc=257, msg='Destination %s does not exist !' % filename)
+        destpath = os.path.dirname(filename)
+        if not os.path.exists(destpath) and not module.check_mode:
+            os.makedirs(destpath)
+        ini_lines = []
+    else:
+        ini_file = open(filename, 'r')
+        try:
+            ini_lines = ini_file.readlines()
+        finally:
+            ini_file.close()
+
+    if module._diff:
+        diff['before'] = ''.join(ini_lines)
+
+    # 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'
     else:
@@ -159,11 +180,13 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese
                         # search backwards for previous non-blank or non-comment line
                         if not re.match(r'^[ \t]*([#;].*)?$', ini_lines[i - 1]):
                             ini_lines.insert(i, assignment_format % (option, value))
+                            msg = 'option added'
                             changed = True
                             break
                 elif state == 'absent' and not option:
                     # remove the entire section
                     del ini_lines[section_start:index]
+                    msg = 'section removed'
                     changed = True
                 break
         else:
@@ -173,6 +196,8 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese
                     if match_opt(option, line):
                         newline = assignment_format % (option, value)
                         changed = ini_lines[index] != newline
+                        if changed:
+                            msg = 'option changed'
                         ini_lines[index] = newline
                         if changed:
                             # remove all possible option occurences from the rest of the section
@@ -191,6 +216,7 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese
                     if match_active_opt(option, line):
                         ini_lines[index] = '#%s' % ini_lines[index]
                         changed = True
+                        msg = 'option changed'
                         break
 
     # remove the fake section line
@@ -200,7 +226,10 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese
         ini_lines.append('[%s]\n' % section)
         ini_lines.append(assignment_format % (option, value))
         changed = True
+        msg = 'section and option added'
 
+    if module._diff:
+        diff['after'] = ''.join(ini_lines)
 
     backup_file = None
     if changed and not module.check_mode:
@@ -212,7 +241,7 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese
         finally:
             ini_file.close()
 
-    return (changed, backup_file)
+    return (changed, backup_file, diff, msg)
 
 # ==============================================================
 # main
@@ -227,7 +256,8 @@ def main():
             value = dict(required=False),
             backup = dict(default='no', type='bool'),
             state = dict(default='present', choices=['present', 'absent']),
-            no_extra_spaces = dict(required=False, default=False, type='bool')
+            no_extra_spaces = dict(required=False, default=False, type='bool'),
+            create=dict(default=False, type='bool')
         ),
         add_file_common_args = True,
         supports_check_mode = True
@@ -240,13 +270,15 @@ def main():
     state = module.params['state']
     backup = module.params['backup']
     no_extra_spaces = module.params['no_extra_spaces']
+    create = module.params['create']
 
-    (changed,backup_file) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces)
+    (changed,backup_file,diff,msg) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces, create)
 
-    file_args = module.load_file_common_arguments(module.params)
-    changed = module.set_fs_attributes_if_different(file_args, changed)
+    if not module.check_mode and os.path.exists(dest):
+        file_args = module.load_file_common_arguments(module.params)
+        changed = module.set_fs_attributes_if_different(file_args, changed)
 
-    results = { 'changed': changed, 'msg': "OK", 'dest': dest }
+    results = { 'changed': changed, 'msg': msg, 'dest': dest, 'diff': diff }
     if backup_file is not None:
         results['backup_file'] = backup_file
 


Test patch: