Repository: ansible/ansible

Issue:
Implicit "setup" task from "gather_facts: yes" is missing attributes compared to explicit task
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
setup

##### ANSIBLE VERSION
```
ansible 2.4.0
```

##### CONFIGURATION
Not relevant

##### OS / ENVIRONMENT
Not relevant

##### SUMMARY
The implicit setup task that is issued from gather_facts differs from an explicit setup task.

The explicit setup task has a data source which makes [task.get_path()](https://github.com/ansible/ansible/blob/178cf6e82e9b446a498c80211302049eae5aae70/lib/ansible/playbook/task.py#L100) (amongst other things) work.

Looking at the ``get_path`` method, you can see that we are expecting the task to have a ``_ds``, ``_data_source`` and ``_line_number`` attributes .

##### STEPS TO REPRODUCE
We can reproduce this issue with a simple callback:
```
from __future__ import (absolute_import, division, print_function)
from ansible.plugins.callback import CallbackBase


class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'notification'
    CALLBACK_NAME = 'setup_task'

    def __init__(self):
        super(CallbackModule, self).__init__()

    def v2_playbook_on_task_start(self, task, is_conditional):
        if hasattr(task, '_ds'):
            print("This task has a data source")
            print(task.get_path())
        else:
            print("This task does not have a data source")
```

And the following playbook:
```
- name: Test playbook
  hosts: all
  gather_facts: yes
  tasks:
    - name: Explicit setup task
      setup:
```

##### EXPECTED RESULTS
I expect the gather_facts implicit setup task to have the same parameters and attributes as an explicit task.

##### ACTUAL RESULTS
```
PLAY [Test playbook] ***********************************************************

TASK [Gathering Facts] *********************************************************
This task does not have a data source
ok: [localhost]

TASK [Explicit setup task] *****************************************************
This task has a data source
/home/dmsimard/dev/ansible-sandbox/playbooks/test.yml:5
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0
```


Discussion:
the attributes ARE there, they are just not set, they should be set to the play that triggers it.
cc @david_obrien
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@dmsimard As @bcoca  mentioned, It has a `_ds` attribute via `task._parent._play`... but the `_ds` attribute does not have `_data_source` and `_linenumber` attributes directly. We have to get them from `ansible_pos`. If core team decides they want to expose the attributes via the `get_path` method, one way would be to make this change in `playbook/task.py`.

BEFORE:
```
    def get_path(self):
        ''' return the absolute path of the task with its line number '''

        path = ""
        if hasattr(self, '_ds') and hasattr(self._ds, '_data_source') and hasattr(self._ds, '_line_number'):
            path = "%s:%s" % (self._ds._data_source, self._ds._line_number)
        return path
```

AFTER:
```
    def get_path(self):
        ''' return the absolute path of the task with its line number '''

        path = ""
        if hasattr(self, '_ds') and hasattr(self._ds, '_data_source') and hasattr(self._ds, '_line_number'):
            path = "%s:%s" % (self._ds._data_source, self._ds._line_number)
        elif self.action in ('setup', 'gather_facts'):
            _play = self._parent._play
            if hasattr(_play, '_ds') and hasattr(_play._ds, '_data_source') and hasattr(_play._ds, '_line_number'):
                path = "%s:%s" % (_play._ds.ansible_pos[0], _play._ds.ansible_pos[1])
        return path
```

 You can get the attributes in a roundabout way with a callback like so:
CALLBACK:
```
[reid@laptop ~/git]$ cat ansible/lib/ansible/plugins/callback/setup.py
from __future__ import (absolute_import, division, print_function)
from ansible.plugins.callback import CallbackBase


class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'notification'
    CALLBACK_NAME = 'setup_task'

    def __init__(self):
        super(CallbackModule, self).__init__()

    def v2_playbook_on_task_start(self, task, is_conditional):
        if hasattr(task, '_ds'):
            print("This task has a data source")
            print(task.get_path())
        else:
            print("This task does not have a data source")
            print("%s:%s" % (task._parent._play._ds.ansible_pos[0], task._parent._play._ds.ansible_pos[1]))
```

RUN (your playbook):
```
TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
This task does not have a data source
/home/reid/git/setup.yml:1
ok: [localhost]

TASK [Explicit setup task] ****************************************************************************************************************************************************************************************
This task has a data source
/home/reid/git/setup.yml:5
ok: [localhost]

PLAY RECAP ********************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0   
```

Resolving patch:
diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py
--- a/lib/ansible/playbook/task.py
+++ b/lib/ansible/playbook/task.py
@@ -106,6 +106,8 @@ def get_path(self):
         path = ""
         if hasattr(self, '_ds') and hasattr(self._ds, '_data_source') and hasattr(self._ds, '_line_number'):
             path = "%s:%s" % (self._ds._data_source, self._ds._line_number)
+        elif hasattr(self._parent._play, '_ds') and hasattr(self._parent._play._ds, '_data_source') and hasattr(self._parent._play._ds, '_line_number'):
+            path = "%s:%s" % (self._parent._play._ds._data_source, self._parent._play._ds._line_number)
         return path
 
     def get_name(self):


Test patch: