Repository: ansible/ansible

Issue:
run_once and smart gathering
<!---
Verify first that your issue/request is not already reported on GitHub.
THIS FORM WILL BE READ BY A MACHINE, COMPLETE ALL SECTIONS AS DESCRIBED.
Also test if the latest release, and devel branch are affected too.
ALWAYS add information AFTER (OUTSIDE) these html comments.
Otherwise it may end up being automatically closed by our bot. -->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Bug Report

##### COMPONENT NAME
<!--- Insert, BELOW THIS COMMENT, the name of the module, plugin, task or feature.
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path-->
run_once

##### ANSIBLE VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "ansible --version" between quotes below -->
```
2.5.0
```

##### CONFIGURATION
<!--- If using Ansible 2.4 or above, paste, BELOW THIS COMMENT, the results of "ansible-config dump --only-changed"
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).-->

```
DEFAULT_GATHERING(/.../ansible.cfg) = smart
```

##### OS / ENVIRONMENT
<!--- Mention, BELOW THIS COMMENT, 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.-->

I guess N/A, but in case:
Origin: Debian stretch, Ubuntu 16.04
Target: Debian Stretch, Debian wheezy

##### SUMMARY
<!--- Explain the problem briefly -->

When the first play implicating several hosts has `run_once`, all the facts of the targeted hosts will be set to the value of the machine it ran on, and if the gathering is smart, they won't be computed again in consequent plays on the same hosts, without `run_once`.

I've seen the latest additions in [the doc](http://docs.ansible.com/ansible/devel/user_guide/playbooks_delegation.html#run-once), but it's absolutely unclear that automatic fact gathering will be impacted by `run_once`, and the fact that smart gathering will persist wrong facts on hosts (and even cache them if cache is enabled) feels really wrong to me.

What would seem right to me is that **only the facts for the host that ran** will be gathered in the play with `run_once`, and then subsequent plays that involve other hosts will gather facts for those hosts.

Another acceptable behavior (to me) would be that implicit facts gathering would be totally unaffected by `run_once`. Facts would be gathered on every host, and then tasks would run on a single host.

##### 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. -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- hosts: all
  run_once: yes
  tasks:
  - debug:
      msg: "{{ ansible_fqdn }}"

- hosts: all
  tasks:
  - debug:
      msg: "{{ ansible_fqdn }}"
```

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
```
PLAY [all] ***********************************************************************************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************************************************************************
ok: [machine-a]

TASK [debug] *********************************************************************************************************************************************************************************
ok: [machine-a] => {}

MSG:

machine-a


PLAY [all] ***********************************************************************************************************************************************************************************

TASK [debug] *********************************************************************************************************************************************************************************
ok: [machine-a] => {}

MSG:

machine-a

ok: [machine-b] => {}

MSG:

machine-b

```
##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->
```
PLAY [all] ***********************************************************************************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************************************************************************
ok: [machine-a]

TASK [debug] *********************************************************************************************************************************************************************************
ok: [machine-a] => {}

MSG:

machine-a


PLAY [all] ***********************************************************************************************************************************************************************************

TASK [debug] *********************************************************************************************************************************************************************************
ok: [machine-a] => {}

MSG:

machine-a

ok: [machine-b] => {}

MSG:

machine-a
```




Discussion:
Files identified in the description:
None

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->

Resolving patch:
diff --git a/lib/ansible/executor/play_iterator.py b/lib/ansible/executor/play_iterator.py
--- a/lib/ansible/executor/play_iterator.py
+++ b/lib/ansible/executor/play_iterator.py
@@ -169,6 +169,9 @@ def __init__(self, inventory, play, play_context, variable_manager, all_vars, st
             fact_path = self._play.fact_path
 
         setup_block = Block(play=self._play)
+        # Gathering facts with run_once would copy the facts from one host to
+        # the others.
+        setup_block.run_once = False
         setup_task = Task(block=setup_block)
         setup_task.action = 'setup'
         setup_task.name = 'Gathering Facts'


Test patch:
diff --git a/test/integration/targets/gathering_facts/runme.sh b/test/integration/targets/gathering_facts/runme.sh
--- a/test/integration/targets/gathering_facts/runme.sh
+++ b/test/integration/targets/gathering_facts/runme.sh
@@ -5,3 +5,5 @@ set -eux
 # ANSIBLE_CACHE_PLUGINS=cache_plugins/ ANSIBLE_CACHE_PLUGIN=none ansible-playbook test_gathering_facts.yml -i ../../inventory -v "$@"
 ansible-playbook test_gathering_facts.yml -i ../../inventory -v "$@"
 #ANSIBLE_CACHE_PLUGIN=base ansible-playbook test_gathering_facts.yml -i ../../inventory -v "$@"
+
+ANSIBLE_GATHERING=smart ansible-playbook test_run_once.yml -i ../../inventory -v "$@"
diff --git a/test/integration/targets/gathering_facts/test_run_once.yml b/test/integration/targets/gathering_facts/test_run_once.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/gathering_facts/test_run_once.yml
@@ -0,0 +1,28 @@
+---
+- hosts: facthost1
+  gather_facts: no
+  tasks:
+    - name: install test local facts
+      copy:
+        src: uuid.fact
+        dest: /etc/ansible/facts.d/
+        mode: 0755
+
+- hosts: facthost1,facthost2
+  gather_facts: yes
+  run_once: yes
+  tasks:
+    - block:
+      - name: 'Check the same host is used'
+        assert:
+          that: 'hostvars.facthost1.ansible_fqdn == hostvars.facthost2.ansible_fqdn'
+          msg: 'This test requires 2 inventory hosts referring to the same host.'
+      - name: "Check that run_once doesn't prevent fact gathering (#39453)"
+        assert:
+          that: 'hostvars.facthost1.ansible_local.uuid != hostvars.facthost2.ansible_local.uuid'
+          msg: "{{ 'Same value for ansible_local.uuid on both hosts: ' ~ hostvars.facthost1.ansible_local.uuid }}"
+      always:
+        - name: remove test local facts
+          file:
+            path: /etc/ansible/facts.d/uuid.fact
+            state: absent
diff --git a/test/integration/targets/gathering_facts/uuid.fact b/test/integration/targets/gathering_facts/uuid.fact
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/gathering_facts/uuid.fact
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import json
+import uuid
+
+
+# return a random string
+print(json.dumps(str(uuid.uuid4())))