Repository: ansible/ansible

Issue:
Allow running task or play in check_mode from playbook
Ansible check_mode allows determining if a task _would_ have changed something, but only on the level of an entire play.

But there are obvious cases where idempotency requires being able to change the order of events - i.e. check if a file would be changed, if so, stop a daemon and change the file - otherwise do nothing.

This can be replicated by comparing mtimes locally and remotely, but leads to complicated syntax for something the existing feature can already provide.

Ideally something like this would be possible:

``` yaml
- hosts: webservers
  tasks:
  - copy: src=monitoredfile dest=monitoredfile
    check_mode: true
    register: copy_check

  - service: name=webserver state=stopped
    when: copy_check.changed

  - copy: src=monitoredfile dest=monitoredfile
    when: copy_check.changed

  - service: name=webserver state=started
    when: copy_check.changed
```

This is simple and readable, and helps in dealing with services which may be monitoring config directories for new files but which would might like to try and avert for changes like that (exim with a perl interpreter can be a problem in this regard).



Discussion:
I wanted to do exactly the same thing and, after some discussion on the list, I submitted https://github.com/ansible/ansible/pull/7439 at mpdehaan's request, but then he decided he didn't want it after all. The code won't apply any more, I imagine, but it should be easy enough to adapt for v2 if you're interested.

This would also make testcases a lot nicer to write. The check_mode support in many modules is flaky, since they are not thoroughly tested. Currently in the integration tests only the template module is included. For some modules (i.e. git) the code in check_mode differs quite a bit from normal mode.


Resolving patch:
diff --git a/lib/ansible/playbook/base.py b/lib/ansible/playbook/base.py
--- a/lib/ansible/playbook/base.py
+++ b/lib/ansible/playbook/base.py
@@ -63,6 +63,7 @@ class Base:
     _always_run           = FieldAttribute(isa='bool')
     _run_once             = FieldAttribute(isa='bool')
     _ignore_errors        = FieldAttribute(isa='bool')
+    _check_mode = FieldAttribute(isa='bool')
 
     # param names which have been deprecated/removed
     DEPRECATED_ATTRIBUTES = [
diff --git a/lib/ansible/playbook/play_context.py b/lib/ansible/playbook/play_context.py
--- a/lib/ansible/playbook/play_context.py
+++ b/lib/ansible/playbook/play_context.py
@@ -434,10 +434,15 @@ def set_task_and_variable_override(self, task, variables, templar):
         # set become defaults if not previouslly set
         task.set_become_defaults(new_info.become, new_info.become_method, new_info.become_user)
 
-        # have always_run override check mode
         if task.always_run:
+            display.deprecated("always_run is deprecated. Use check_mode = no instead.", version="2.4", removed=False)
             new_info.check_mode = False
 
+        # check_mode replaces always_run, overwrite always_run if both are given
+        if task.check_mode is not None:
+            new_info.check_mode = task.check_mode
+
+
         return new_info
 
     def make_become_cmd(self, cmd, executable=None):


Test patch:
diff --git a/docsite/rst/test_strategies.rst b/docsite/rst/test_strategies.rst
--- a/docsite/rst/test_strategies.rst
+++ b/docsite/rst/test_strategies.rst
@@ -42,7 +42,7 @@ existing system, using the `--check` flag to the `ansible` command will report i
 bring the system into a desired state.
 
 This can let you know up front if there is any need to deploy onto the given system.  Ordinarily scripts and commands don't run in check mode, so if you
-want certain steps to always execute in check mode, such as calls to the script module, add the 'always_run' flag::
+want certain steps to always execute in check mode, such as calls to the script module, disable check mode for those tasks::
 
 
    roles:
@@ -50,7 +50,7 @@ want certain steps to always execute in check mode, such as calls to the script
 
    tasks:
      - script: verify.sh
-       always_run: True
+       check_mode: no
 
 Modules That Are Useful for Testing
 ```````````````````````````````````
diff --git a/test/integration/roles/test_always_run/tasks/main.yml b/test/integration/roles/test_always_run/tasks/main.yml
--- a/test/integration/roles/test_always_run/tasks/main.yml
+++ b/test/integration/roles/test_always_run/tasks/main.yml
@@ -18,7 +18,7 @@
 
 - name: run a command while in check mode
   shell: echo "running"
-  always_run: yes
+  check_mode: no
   register: result
 
 - name: assert that the command was run
diff --git a/test/integration/roles/test_check_mode/tasks/main.yml b/test/integration/roles/test_check_mode/tasks/main.yml
--- a/test/integration/roles/test_check_mode/tasks/main.yml
+++ b/test/integration/roles/test_check_mode/tasks/main.yml
@@ -17,23 +17,30 @@
 # along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 
 - name: fill in a basic template in check mode
-  template: src=foo.j2 dest={{output_dir}}/foo.templated mode=0644
+  template: src=foo.j2 dest={{output_dir}}/checkmode_foo.templated mode=0644
   register: template_result
 
+- name: check whether file exists
+  stat: path={{output_dir}}/checkmode_foo.templated
+  register: foo
+
 - name: verify that the file was marked as changed in check mode
   assert: 
     that: 
-      - "template_result.changed == true"
+      - "template_result|changed"
+      - "not foo.stat.exists"
 
-- name: Actually create the file
-  template: src=foo.j2 dest={{output_dir}}/foo.templated2 mode=0644
-  always_run: True
+- name: Actually create the file, disable check mode
+  template: src=foo.j2 dest={{output_dir}}/checkmode_foo.templated2 mode=0644
+  check_mode: no
+  register: checkmode_disabled
 
-- name: fill in a basic template in check mode
-  template: src=foo.j2 dest={{output_dir}}/foo.templated2 mode=0644
+- name: fill in template with new content
+  template: src=foo.j2 dest={{output_dir}}/checkmode_foo.templated2 mode=0644
   register: template_result2
 
-- name: verify that the file was marked as not changed in check mode
+- name: verify that the file was not changed
   assert: 
     that: 
-      - "template_result2.changed == false"
+      - "checkmode_disabled|changed"
+      - "not template_result2|changed"
diff --git a/test/integration/roles/test_template/tasks/main.yml b/test/integration/roles/test_template/tasks/main.yml
--- a/test/integration/roles/test_template/tasks/main.yml
+++ b/test/integration/roles/test_template/tasks/main.yml
@@ -179,3 +179,49 @@
     that:
       - "template_result.changed == False"
 
+
+# check_mode
+
+- name: fill in a basic template in check mode
+  template: src=short.j2 dest={{output_dir}}/short.templated
+  register: template_result
+  check_mode: True
+
+- name: check file exists
+  stat: path={{output_dir}}/short.templated
+  register: templated
+
+- name: verify that the file was marked as changed in check mode but was not created
+  assert: 
+    that: 
+      - "not templated.stat.exists"
+      - "template_result|changed"
+
+- name: fill in a basic template
+  template: src=short.j2 dest={{output_dir}}/short.templated
+
+- name: fill in a basic template in check mode
+  template: src=short.j2 dest={{output_dir}}/short.templated
+  register: template_result
+  check_mode: True
+
+- name: verify that the file was marked as not changes in check mode
+  assert: 
+    that: 
+      - "not template_result|changed"
+      - "'templated_var_loaded' in lookup('file', '{{output_dir | expanduser}}/short.templated')"
+
+- name: change var for the template
+  set_fact: 
+    templated_var: "changed"
+
+- name: fill in a basic template with changed var in check mode
+  template: src=short.j2 dest={{output_dir}}/short.templated
+  register: template_result
+  check_mode: True
+
+- name: verify that the file was marked as changed in check mode but the content was not changed
+  assert: 
+    that: 
+      - "'templated_var_loaded' in lookup('file', '{{output_dir | expanduser }}/short.templated')"
+      - "template_result|changed"
diff --git a/test/integration/roles/test_template/templates/short.j2 b/test/integration/roles/test_template/templates/short.j2
new file mode 100644
--- /dev/null
+++ b/test/integration/roles/test_template/templates/short.j2
@@ -0,0 +1 @@
+{{ templated_var }}