Repository: ansible/ansible

Issue:
when stdout_callback=actionable task names with variables write to log without values
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
callback_plugins        = plugins
stdout_callback         = actionable

##### ANSIBLE VERSION
```
ansible-2.5.0-100.git201709282105.3e7b24069.devel.el7.centos
```

##### CONFIGURATION
```
ANSIBLE_SSH_ARGS(/home/bond/_ansible_bug_call/ansible.cfg) = -o ControlMaster=auto -o ControlPersist=600s -o UserKnownHostsFile=/dev/null
ANSIBLE_SSH_CONTROL_PATH(/home/bond/_ansible_bug_call/ansible.cfg) = /tmp/ansible-ssh-%%h-%%p-%%r
ANSIBLE_SSH_PIPELINING(/home/bond/_ansible_bug_call/ansible.cfg) = True
DEFAULT_CALLBACK_PLUGIN_PATH(/home/bond/_ansible_bug_call/ansible.cfg) = [u'/home/bond/_ansible_bug_call/plugins']
DEFAULT_FORKS(/home/bond/_ansible_bug_call/ansible.cfg) = 100
DEFAULT_LOG_PATH(/home/bond/_ansible_bug_call/ansible.cfg) = /home/bond/_ansible_bug_call/logs/ansible.log
DEFAULT_REMOTE_PORT(/home/bond/_ansible_bug_call/ansible.cfg) = 48022
DEFAULT_REMOTE_USER(/home/bond/_ansible_bug_call/ansible.cfg) = ansible
DEFAULT_ROLES_PATH(/home/bond/_ansible_bug_call/ansible.cfg) = [u'/home/bond/_ansible_bug_call/core/roles']
DEFAULT_TIMEOUT(/home/bond/_ansible_bug_call/ansible.cfg) = 120
HOST_KEY_CHECKING(/home/bond/_ansible_bug_call/ansible.cfg) = False
PARAMIKO_RECORD_HOST_KEYS(/home/bond/_ansible_bug_call/ansible.cfg) = False
RETRY_FILES_ENABLED(/home/bond/_ansible_bug_call/ansible.cfg) = True
```
##### OS / ENVIRONMENT
Centos 7.4

##### SUMMARY
When i use stdout_callback=actionable task name with variables in screen output and log displayed with variable name, not variable value

##### STEPS TO REPRODUCE
clone test playbook
https://github.com/LuckySB/ansible_bug_stdout_callback_actionable
run test.sh

```
git clone https://github.com/LuckySB/ansible_bug_stdout_callback_actionable
./test.sh

# uncomment stdout_callback=actionable in ansible.cfg
# and run again
./test.sh
```

##### EXPECTED RESULTS
with and without stdout_callback=actionable
```
PLAY [localhost] ******************************************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************************************
ok: [localhost]

TASK [test : Normal name] *********************************************************************************************************************************************
ok: [localhost] => {
    "msg": "Normal name"
}

TASK [test : Name with number vars 42] ********************************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with number vars 42"
}

TASK [test : Name with string vars 100] *******************************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with string vars 100"
}

TASK [test : Name with function vars 2] *******************************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with function vars 2"
}

PLAY RECAP ************************************************************************************************************************************************************
localhost                  : ok=5    changed=4    unreachable=0    failed=0

```

##### ACTUAL RESULTS
without stdout_callback=actionable
```
PLAY [localhost] ******************************************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************************************
ok: [localhost]

TASK [test : Normal name] *********************************************************************************************************************************************
ok: [localhost] => {
    "msg": "Normal name"
}

TASK [test : Name with number vars 42] ********************************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with number vars 42"
}

TASK [test : Name with string vars 100] *******************************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with string vars 100"
}

TASK [test : Name with function vars 2] *******************************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with function vars 2"
}

PLAY RECAP ************************************************************************************************************************************************************
localhost                  : ok=5    changed=4    unreachable=0    failed=0

```

with stdout_callback=actionable
```
PLAY [localhost] ******************************************************************************************************************************************************

TASK [test : Normal name] *********************************************************************************************************************************************
ok: [localhost] => {
    "msg": "Normal name"
}

TASK [test : Name with number vars {{number_var}}] ********************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with number vars 42"
}

TASK [test : Name with string vars {{string_var}}] ********************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with string vars 100"
}

TASK [test : Name with function vars {{number_float_var1}}] ***********************************************************************************************************
ok: [localhost] => {
    "msg": "Name with function vars 2"
}

PLAY RECAP ************************************************************************************************************************************************************
localhost                  : ok=5    changed=4    unreachable=0    failed=0

```



Discussion:
cc @dharmabumstead
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
cc @agaffney
I don't see any obvious reason why this would be the case. However, I haven't done much more than just eyeball the code.
I am able to reproduce this in `2.4.0.0`, current `stable-2.3`, and current `devel`.
This seems to be happening *outside* of the `actionable` callback plugin. When `task.get_name()` is called in `ansible.plugins.callback.default.v2_playbook_on_task_start()` via the `default` plugin, the templates are evaluated. That same call returns a non-templated string when it's called via the `actionable` plugin.
And now we get to something interesting. It looks like the `name` attribute in the `Task` may or may not have the template evaluated at any given time. I added some debugging code in the `ansible.playbook.Task.get_name()` function to display the current object address and the value of `self.name`.

`default`:

```
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars 42'

TASK [test : Name with number vars 42] ******************************************************************************************************************************************
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars 42'
Task(0x7fa3624b8410).get_name(): self.name = 'Name with number vars 42'
ok: [localhost] => {
    "msg": "Name with number vars 42"
}
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars 100'

TASK [test : Name with string vars 100] *****************************************************************************************************************************************
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars 100'
Task(0x7fa3624b8490).get_name(): self.name = 'Name with string vars 100'
ok: [localhost] => {
    "msg": "Name with string vars 100"
}
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars 2'

TASK [test : Name with function vars 2] *****************************************************************************************************************************************
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars 2'
Task(0x7fa3624b8510).get_name(): self.name = 'Name with function vars 2'
ok: [localhost] => {
    "msg": "Name with function vars 2"
}
```

`actionable`:

```
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars 42'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars 42'
Task(0x7fd5aa3a83d0).get_name(): self.name = 'Name with number vars {{number_var}}'

TASK [test : Name with number vars {{number_var}}] ******************************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with number vars 42"
}
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars 100'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars 100'
Task(0x7fd5aa3a8450).get_name(): self.name = 'Name with string vars {{string_var}}'

TASK [test : Name with string vars {{string_var}}] ******************************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with string vars 100"
}
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars 2'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars 2'
Task(0x7fd5aa3a84d0).get_name(): self.name = 'Name with function vars {{number_float_var1}}'

TASK [test : Name with function vars {{number_float_var1}}] *********************************************************************************************************************
ok: [localhost] => {
    "msg": "Name with function vars 2"
}
```
@bcoca I seem to have reached a point in my debugging where two separate denizens of the deep, dark nether reaches of the Ansible code are conspiring against me: templating and play/block/task `FieldAttribute`. I can dig no further without a bit of guidance and a large machete.
It appears that this can be addressed with:

```
diff --git a/lib/ansible/plugins/callback/actionable.py b/lib/ansible/plugins/callback/actionable.py
index d0127a3..2dc47af 100644
--- a/lib/ansible/plugins/callback/actionable.py
+++ b/lib/ansible/plugins/callback/actionable.py
@@ -20,6 +20,7 @@ DOCUMENTATION = '''
       - set as stdout callback in configuration
 '''
 
+import copy
 from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
 
 
@@ -40,7 +41,7 @@ class CallbackModule(CallbackModule_default):
         self.shown_title = True
 
     def v2_playbook_on_task_start(self, task, is_conditional):
-        self.last_task = task
+        self.last_task = copy.deepcopy(task)
         self.shown_title = False
 
     def display_task_banner(self):
```

It's unclear if doing that in this plugin is the correct approach, though. @bcoca 
may be the case in the order of the task
at default the output is generated in the order of execution

With the actionable plugin, the output is generated after the task execution, when the task variables have already been cleared

@agaffney, your patch works, but playbook execution time increases up to 3 times:
```
$ export ANSIBLE_STDOUT_CALLBACK=actionable
$ time ansible-playbook test.yml
3.93s user 0.58s system 67% cpu 6.645 total

$ export ANSIBLE_STDOUT_CALLBACK=actionable_custom
$ time ansible-playbook test.yml
13.22s user 1.14s system 80% cpu 17.751 total
```
Tried several times.

I suggest more simple fix:
``` python
    def v2_playbook_on_task_start(self, task, is_conditional):
        self.last_task = task
        self.last_task_name = task.name
        self.shown_title = False

    def display_task_banner(self):
        if not self.shown_title:
            self.last_task.name = self.last_task_name
            self.super_ref.v2_playbook_on_task_start(self.last_task, None)
            self.shown_title = True
```
While that would technically work, I feel like that's just asking for weird side effects. We know from my debugging above that the Task object reference we get on task start can change, or even be re-used, by the time that we're using it to display the task banner. This implies that explicitly setting attributes on this object at the time of display could cause unexpected results.
There's a few different "proper" fixes that we could do if we also modified `default.py`. The easiest would probably be something like making `_print_task_banner()` return the constructed task banner, rather than printing it right there. Then `actionable.py` could store it and spit out a static string at the proper time.
can you check against current devel? I believe #27175 should have fixed this in a more general manner.
not fixed ((

i make rpm from current devel, and try run playbook from https://github.com/LuckySB/ansible_bug_stdout_callback_actionable

and get in out 
```
TASK [test : Name with number vars {{number_var}}] 
TASK [test : Name with string vars {{string_var}}]
TASK [test : Name with function vars {{number_float_var1}}]
```

Version of python modules in Centos7:
may be this old version of python-jinja2-2.7 issue ?

```Installing:
 ansible                noarch   2.5.0-100.git201710280835.2e12a5f8c2.devel.el7.centos   /ansible-2.5.0-100.git201710280835.2e12a5f8c2.devel.el7.centos.noarch    39 M
Installing for dependencies:
 PyYAML                 x86_64   3.10-11.el7                                             base                                                                    153 k
 libyaml                x86_64   0.1.4-11.el7_0                                          base                                                                     55 k
 python-cffi            x86_64   1.6.0-5.el7                                             base                                                                    218 k
 python-enum34          noarch   1.0.4-1.el7                                             base                                                                     52 k
 python-idna            noarch   2.4-1.el7                                               base                                                                     94 k
 python-ipaddress       noarch   1.0.16-2.el7                                            base                                                                     34 k
 python-jinja2          noarch   2.7.2-2.el7                                             base                                                                    515 k
 python-paramiko        noarch   2.1.1-2.el7                                             extras                                                                  267 k
 python-ply             noarch   3.4-11.el7                                              base                                                                    123 k
 python-pycparser       noarch   2.14-1.el7                                              base                                                                    104 k
 python-six             noarch   1.9.0-2.el7                                             base                                                                     29 k
 python2-cryptography   x86_64   1.7.2-1.el7_4.1                                         updates                                                                 502 k
 python2-pyasn1         noarch   0.1.9-7.el7                                             base                                                                    100 k
 sshpass                x86_64   1.06-2.el7                                              extras                                                                   21 k
```
test with ansible-2.6.0-100.git201803241800.d502745a4e.devel
not fixed

Resolving patch:
diff --git a/lib/ansible/plugins/callback/actionable.py b/lib/ansible/plugins/callback/actionable.py
--- a/lib/ansible/plugins/callback/actionable.py
+++ b/lib/ansible/plugins/callback/actionable.py
@@ -20,6 +20,7 @@
       - set as stdout callback in configuration
 '''
 
+from ansible import constants as C
 from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
 
 
@@ -33,6 +34,7 @@ def __init__(self):
         self.super_ref = super(CallbackModule, self)
         self.super_ref.__init__()
         self.last_task = None
+        self.last_task_banner = None
         self.shown_title = False
 
     def v2_playbook_on_handler_task_start(self, task):
@@ -41,6 +43,7 @@ def v2_playbook_on_handler_task_start(self, task):
 
     def v2_playbook_on_task_start(self, task, is_conditional):
         self.last_task = task
+        self.last_task_banner = self._get_task_banner(task)
         self.shown_title = False
 
     def display_task_banner(self):
@@ -48,6 +51,33 @@ def display_task_banner(self):
             self.super_ref.v2_playbook_on_task_start(self.last_task, None)
             self.shown_title = True
 
+    def _print_task_banner(self, task):
+        self._display.banner(self.last_task_banner)
+        self._print_task_path(self.last_task)
+        self._last_task_banner = self.last_task._uuid
+
+    def _print_task_path(self, task):
+        if self._display.verbosity >= 2:
+            path = task.get_path()
+            if path:
+                self._display.display(u"task path: %s" % path, color=C.COLOR_DEBUG)
+
+    def _get_task_banner(self, task):
+        # args can be specified as no_log in several places: in the task or in
+        # the argument spec.  We can check whether the task is no_log but the
+        # argument spec can't be because that is only run on the target
+        # machine and we haven't run it thereyet at this time.
+        #
+        # So we give people a config option to affect display of the args so
+        # that they can secure this if they feel that their stdout is insecure
+        # (shoulder surfing, logging stdout straight to a file, etc).
+        args = ''
+        if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT:
+            args = u', '.join(u'%s=%s' % a for a in task.args.items())
+            args = u' %s' % args
+
+        return u"TASK [%s%s]" % (task.get_name().strip(), args)
+
     def v2_runner_on_failed(self, result, ignore_errors=False):
         self.display_task_banner()
         self.super_ref.v2_runner_on_failed(result, ignore_errors)


Test patch: