Repository: ansible/ansible

Issue:
local_action: include_role fails with 'this is probably a bug'
<!---
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
local_action: iclude_role

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
$ ansible --version
ansible 2.4.0 (devel b35f39479b) last updated 2017/08/30 15:12:04 (GMT +200)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/user/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/user/Repos/ansible/lib/ansible
  executable location = /home/user/Repos/ansible/bin/ansible
  python version = 2.7.6 (default, Oct 26 2016, 20:30:19) [GCC 4.8.4]
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
Ubuntu 14.04
##### SUMMARY
<!--- Explain the problem briefly -->
You cannot have a task that runs a role as local_action.

A workaround is `delegate_to: 127.0.0.1`.


##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->
```
.
├── playbook.yml
└── roles
    └── test
        └── tasks
            └── main.yml
```

* playbook.yml
```yaml
---

- name: Local action include role
  hosts: all
  tasks:
    - name: Trigger bug
      local_action: include_role name=test
```
* main.yml
```yaml
---

- debug: msg="will not be printed"
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
Ansible should execute the role and print the message 'will not be printed'
##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
$ ansible-playbook -i "localhost," -c local playbook.yml -vvv
ansible-playbook 2.4.0 (devel b35f39479b) last updated 2017/08/30 15:12:04 (GMT +200)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/user/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/user/Repos/ansible/lib/ansible
  executable location = /home/user/Repos/ansible/bin/ansible-playbook
  python version = 2.7.6 (default, Oct 26 2016, 20:30:19) [GCC 4.8.4]
Using /etc/ansible/ansible.cfg as config file
Parsed localhost, inventory source with host_list plugin

PLAYBOOK: playbook.yml ************************************************************************
1 plays in playbook.yml

PLAY [Local action include role] **************************************************************

TASK [Gathering Facts] ************************************************************************
Using module file /home/user/Repos/ansible/lib/ansible/modules/system/setup.py
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: user
<localhost> EXEC /bin/sh -c 'echo ~ && sleep 0'
<localhost> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/user/.ansible/tmp/ansible-tmp-1504099569.5-119095676611608 `" && echo ansible-tmp-1504099569.5-119095676611608="` echo /home/user/.ansible/tmp/ansible-tmp-1504099569.5-119095676611608 `" ) && sleep 0'
<localhost> PUT /tmp/tmpnGFBA0 TO /home/user/.ansible/tmp/ansible-tmp-1504099569.5-119095676611608/setup.py
<localhost> EXEC /bin/sh -c 'chmod u+x /home/user/.ansible/tmp/ansible-tmp-1504099569.5-119095676611608/ /home/user/.ansible/tmp/ansible-tmp-1504099569.5-119095676611608/setup.py && sleep 0'
<localhost> EXEC /bin/sh -c '/usr/bin/python /home/user/.ansible/tmp/ansible-tmp-1504099569.5-119095676611608/setup.py; rm -rf "/home/user/.ansible/tmp/ansible-tmp-1504099569.5-119095676611608/" > /dev/null 2>&1 && sleep 0'
ok: [localhost]
META: ran handlers

TASK [Trigger bug] ****************************************************************************
task path: /home/user/Test/ansible-block/playbook.yml:6
 [WARNING]: Failure using method (v2_runner_on_ok) in callback plugin
(<ansible.plugins.callback.default.CallbackModule object at 0x7fbe3cf06ad0>): TASK: Trigger
bug is not JSON serializable

ERROR! Unexpected Exception, this is probably a bug: 'Task' object has no attribute 'get_block_list'
the full traceback was:

Traceback (most recent call last):
  File "/home/user/Repos/ansible/bin/ansible-playbook", line 106, in <module>
    exit_code = cli.run()
  File "/home/user/Repos/ansible/lib/ansible/cli/playbook.py", line 130, in run
    results = pbex.run()
  File "/home/user/Repos/ansible/lib/ansible/executor/playbook_executor.py", line 154, in run
    result = self._tqm.run(play=play)
  File "/home/user/Repos/ansible/lib/ansible/executor/task_queue_manager.py", line 289, in run
    play_return = strategy.run(iterator, play_context)
  File "/home/user/Repos/ansible/lib/ansible/plugins/strategy/linear.py", line 318, in run
    blocks, handler_blocks = new_ir.get_block_list(play=iterator._play, variable_manager=self._variable_manager, loader=self._loader)
AttributeError: 'Task' object has no attribute 'get_block_list'
```



Discussion:
cc @bcoca
~~is there any workaround?~~ I got past this using `delegate_to: 127.0.0.1` instead of local_action
I've gotten this now too

OS Environment: WSL Ubuntu 14.04.5 LTS
Ansible version: 2.4.1.0
python version = 2.7.6 (default, Nov 23 2017, 15:49:48) [GCC 4.8.4]

```
task path: /mnt/c/git/ansible/plays/website_unarchive.yml:41
 [WARNING]: Failure using method (v2_runner_on_ok) in callback plugin
(<ansible.plugins.callback.default.CallbackModule object at 0x7f985311b990>): TASK: Ensure db is present is not
JSON serializable

Callback Exception:
  File "/usr/lib/python2.7/dist-packages/ansible/executor/task_queue_manager.py", line 376, in send_callback
    method(*args, **kwargs)
   File "/usr/lib/python2.7/dist-packages/ansible/plugins/callback/default.py", line 98, in v2_runner_on_ok
    msg += " => %s" % (self._dump_results(result._result),)
   File "/usr/lib/python2.7/dist-packages/ansible/plugins/callback/__init__.py", line 109, in _dump_results
    return json.dumps(abridged_result, indent=indent, ensure_ascii=False, sort_keys=sort_keys)
   File "/usr/lib/python2.7/json/__init__.py", line 250, in dumps
    sort_keys=sort_keys, **kw).encode(obj)
   File "/usr/lib/python2.7/json/encoder.py", line 209, in encode
    chunks = list(chunks)
   File "/usr/lib/python2.7/json/encoder.py", line 434, in _iterencode
    for chunk in _iterencode_dict(o, _current_indent_level):
   File "/usr/lib/python2.7/json/encoder.py", line 408, in _iterencode_dict
    for chunk in chunks:
   File "/usr/lib/python2.7/json/encoder.py", line 442, in _iterencode
    o = _default(o)
   File "/usr/lib/python2.7/json/encoder.py", line 184, in default
    raise TypeError(repr(o) + " is not JSON serializable")

ERROR! Unexpected Exception, this is probably a bug: 'Task' object has no attribute 'get_block_list'
the full traceback was:

Traceback (most recent call last):
  File "/usr/bin/ansible-playbook", line 106, in <module>
    exit_code = cli.run()
  File "/usr/lib/python2.7/dist-packages/ansible/cli/playbook.py", line 130, in run
    results = pbex.run()
  File "/usr/lib/python2.7/dist-packages/ansible/executor/playbook_executor.py", line 154, in run
    result = self._tqm.run(play=play)
  File "/usr/lib/python2.7/dist-packages/ansible/executor/task_queue_manager.py", line 302, in run
    play_return = strategy.run(iterator, play_context)
  File "/usr/lib/python2.7/dist-packages/ansible/plugins/strategy/linear.py", line 331, in run
    new_blocks, handler_blocks = new_ir.get_block_list(
AttributeError: 'Task' object has no attribute 'get_block_list'
```

What is confusing is that it WAS working not too long ago, but I can't identify the change that may have caused this.

I had made the switch to using a dynamic inventory as the only memorable thing I've changed recently.

Resolving patch:
diff --git a/lib/ansible/playbook/helpers.py b/lib/ansible/playbook/helpers.py
--- a/lib/ansible/playbook/helpers.py
+++ b/lib/ansible/playbook/helpers.py
@@ -22,7 +22,9 @@
 
 from ansible import constants as C
 from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound, AnsibleAssertionError
+from ansible.module_utils._text import to_native
 from ansible.module_utils.six import string_types
+from ansible.parsing.mod_args import ModuleArgsParser
 
 try:
     from __main__ import display
@@ -118,7 +120,18 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
             )
             task_list.append(t)
         else:
-            if 'include' in task_ds or 'import_tasks' in task_ds or 'include_tasks' in task_ds:
+            args_parser = ModuleArgsParser(task_ds)
+            try:
+                (action, args, delegate_to) = args_parser.parse()
+            except AnsibleParserError as e:
+                # if the raises exception was created with obj=ds args, then it includes the detail
+                # so we dont need to add it so we can just re raise.
+                if e._obj:
+                    raise
+                # But if it wasn't, we can add the yaml object now to get more detail
+                raise AnsibleParserError(to_native(e), obj=task_ds, orig_exc=e)
+
+            if action in ('include', 'import_tasks', 'include_tasks'):
 
                 if use_handlers:
                     include_class = HandlerTaskInclude
@@ -140,9 +153,9 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
                 # check to see if this include is dynamic or static:
                 # 1. the user has set the 'static' option to false or true
                 # 2. one of the appropriate config options was set
-                if 'include_tasks' in task_ds:
+                if action == 'include_tasks':
                     is_static = False
-                elif 'import_tasks' in task_ds:
+                elif action == 'import_tasks':
                     is_static = True
                 elif t.static is not None:
                     display.deprecated("The use of 'static' has been deprecated. "
@@ -155,7 +168,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
 
                 if is_static:
                     if t.loop is not None:
-                        if 'import_tasks' in task_ds:
+                        if action == 'import_tasks':
                             raise AnsibleParserError("You cannot use loops on 'import_tasks' statements. You should use 'include_tasks' instead.", obj=task_ds)
                         else:
                             raise AnsibleParserError("You cannot use 'static' on an include with a loop", obj=task_ds)
@@ -262,7 +275,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
                         tags = tags.split(',')
 
                     if len(tags) > 0:
-                        if 'include_tasks' in task_ds or 'import_tasks' in task_ds:
+                        if action in ('include_tasks', 'import_tasks'):
                             raise AnsibleParserError('You cannot specify "tags" inline to the task, it is a task keyword')
                         if len(ti_copy.tags) > 0:
                             raise AnsibleParserError(
@@ -292,7 +305,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
                     t.is_static = False
                     task_list.append(t)
 
-            elif 'include_role' in task_ds or 'import_role' in task_ds:
+            elif action in ('include_role', 'import_role'):
                 ir = IncludeRole.load(
                     task_ds,
                     block=block,
@@ -305,7 +318,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
                 #   1. the user has set the 'static' option to false or true
                 #   2. one of the appropriate config options was set
                 is_static = False
-                if 'import_role' in task_ds:
+                if action == 'import_role':
                     is_static = True
 
                 elif ir.static is not None:
@@ -315,7 +328,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
 
                 if is_static:
                     if ir.loop is not None:
-                        if 'import_role' in task_ds:
+                        if action == 'import_role':
                             raise AnsibleParserError("You cannot use loops on 'import_role' statements. You should use 'include_role' instead.", obj=task_ds)
                         else:
                             raise AnsibleParserError("You cannot use 'static' on an include_role with a loop", obj=task_ds)


Test patch:
diff --git a/test/integration/targets/include_import/tasks/test_include_tasks.yml b/test/integration/targets/include_import/tasks/test_include_tasks.yml
--- a/test/integration/targets/include_import/tasks/test_include_tasks.yml
+++ b/test/integration/targets/include_import/tasks/test_include_tasks.yml
@@ -39,3 +39,6 @@
           assert:
             that:
               - set_in_tasks4
+
+    - name: include_tasks + action
+      action: include_tasks tasks1.yml