96

I have an Ansible playbook, where I would like a variable I register in a first play targeted on one node to be available in a second play, targeted on another node.

Here is the playbook I am using:

---
- hosts: localhost
  gather_facts: no

  tasks:
    - command: echo "hello world"
      register: foo


- hosts: main
  gather_facts: no

  tasks:
    - debug:
        msg: {{ foo.stdout }}

But, when I try to access the variable in the second play, targeted on main, I get this message:

The task includes an option with an undefined variable. The error was: 'foo' is undefined

How can I access foo, registered on localhost, from main?

β.εηοιτ.βε
  • 33,893
  • 13
  • 69
  • 83
Chris Adams
  • 2,721
  • 5
  • 33
  • 45

5 Answers5

165

The problem you're running into is that you're trying to reference facts/variables of one host from those of another host.

You need to keep in mind that in Ansible, the variable foo assigned to the host localhost is distinct from the variable foo assigned to the host main or any other host.
If you want to access one hosts facts/variables from another host then you need to explicitly reference it via the hostvars variable. There's a bit more of a discussion on this in this question.

Suppose you have a playbook like this:

- hosts: localhost
  gather_facts: no

  tasks:   
    - command: echo "hello world"
      register: foo


- hosts: localhost
  gather_facts: no

  tasks:
    - debug: 
        var: foo

This will work because you're referencing the host localhost and localhosts's instance of the variable foo in both plays.

The output of this playbook is something like this:

PLAY [localhost] **************************************************

TASK: [command] ***************************************************
changed: [localhost]

PLAY [localhost] **************************************************

TASK: [debug] *****************************************************
ok: [localhost] => {
    "var": {
        "foo": {
            "changed": true,
            "cmd": [
                "echo",
                "hello world"
            ],
            "delta": "0:00:00.004585",
            "end": "2015-11-24 20:49:27.462609",
            "invocation": {
                "module_args": "echo \"hello world\",
                "module_complex_args": {},
                "module_name": "command"
            },
            "rc": 0,
            "start": "2015-11-24 20:49:27.458024",
            "stderr": "",
            "stdout": "hello world",
            "stdout_lines": [
                "hello world"
            ],
            "warnings": []
        }
    }
}

If you modify this playbook slightly to run the first play on one host and the second play on a different host, you'll get the error that you encountered.

Solution

The solution is to use Ansible's built-in hostvars variable to have the second host explicitly reference the first hosts variable.

So modify the first example like this:

- hosts: localhost
  gather_facts: no

  tasks:
    - command: echo "hello world"
      register: foo


- hosts: main
  gather_facts: no

  tasks:
    - debug: 
        var: foo
      when: foo is defined

    - debug: 
        var: hostvars['localhost']['foo']
        ## alternatively, you can use:
        # var: hostvars.localhost.foo
      when: hostvars['localhost']['foo'] is defined

The output of this playbook shows that the first task is skipped because foo is not defined by the host main.
But the second task succeeds because it's explicitly referencing localhosts's instance of the variable foo:

TASK: [debug] *************************************************
skipping: [main]

TASK: [debug] *************************************************
ok: [main] => {
    "var": {
        "hostvars['localhost']['foo']": {
            "changed": true,
            "cmd": [
                "echo",
                "hello world"
            ],
            "delta": "0:00:00.005950",
            "end": "2015-11-24 20:54:04.319147",
            "invocation": {
                "module_args": "echo \"hello world\"",
                "module_complex_args": {},
                "module_name": "command"
            },
            "rc": 0,
            "start": "2015-11-24 20:54:04.313197",
            "stderr": "",
            "stdout": "hello world",
            "stdout_lines": [
                "hello world"
            ],
            "warnings": []
        }
    }
}

So, in a nutshell, you want to modify the variable references in your main playbook to reference the localhost variables in this manner:

{{ hostvars['localhost']['foo'] }}
{# alternatively, you can use: #}
{{ hostvars.localhost.foo }}
Tms91
  • 3,456
  • 6
  • 40
  • 74
Bruce P
  • 19,995
  • 8
  • 63
  • 73
  • 12
    Great answer! Something like this should be in documentation. – StalkAlex Jun 08 '16 at 12:22
  • 1
    if you are using host from an inventory - something like this should help - "{% for host in groups['db_servers'] %}{% if loop.index < 2 %}{{hostvars[host]['myvar']}}{% endif %}{% endfor %}" – Hrishikesh Kumar Feb 13 '17 at 08:20
  • 1
    [somewhere in the extremely elusive document](https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#fact-caching) – Gerald Jul 20 '18 at 07:26
  • I thought about this...but I'm wondering, using variable precedence and scope, wouldn't using group_vars solve the issue? Can we modify group vars via set_facts? I don't think so given that set_facts scope is WITHIN the host the play runs in. The next question is, is there any way at all to modify group_vars at run time or is this hostvars the only method available because what if a play doesn't run and that variable doesn't get set for some reason within the previous play. I want to be able to access it and modify it from another play/host. One variable for all, rather than one per host. – eco Apr 12 '19 at 21:52
  • @StalkAlex it is though https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#accessing-information-about-other-hosts-with-magic-variables – Jean-Bernard Jansen May 08 '19 at 22:50
  • @CodeMed, as of today, this works from `anotherhost` to `yetanotherhost` – Mateus Terra Dec 27 '20 at 23:39
74

Use a dummy host and its variables

For example, to pass a Kubernetes token and hash from the master to the workers.

On master

- name: "Cluster token"
  shell: kubeadm token list | cut -d ' ' -f1 | sed -n '2p'
  register: K8S_TOKEN

- name: "CA Hash"
  shell: openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'
  register: K8S_MASTER_CA_HASH

- name: "Add K8S Token and Hash to dummy host"
  add_host:
    name:   "K8S_TOKEN_HOLDER"
    token:  "{{ K8S_TOKEN.stdout }}"
    hash:   "{{ K8S_MASTER_CA_HASH.stdout }}"

- name:
  debug:
    msg: "[Master] K8S_TOKEN_HOLDER K8S token is {{ hostvars['K8S_TOKEN_HOLDER']['token'] }}"

- name:
  debug:
    msg: "[Master] K8S_TOKEN_HOLDER K8S Hash is  {{ hostvars['K8S_TOKEN_HOLDER']['hash'] }}"

On worker

- name:
  debug:
    msg: "[Worker] K8S_TOKEN_HOLDER K8S token is {{ hostvars['K8S_TOKEN_HOLDER']['token'] }}"

- name:
  debug:
    msg: "[Worker] K8S_TOKEN_HOLDER K8S Hash is  {{ hostvars['K8S_TOKEN_HOLDER']['hash'] }}"

- name: "Kubeadmn join"
  shell: >
    kubeadm join --token={{ hostvars['K8S_TOKEN_HOLDER']['token'] }}
    --discovery-token-ca-cert-hash sha256:{{ hostvars['K8S_TOKEN_HOLDER']['hash'] }}
    {{K8S_MASTER_NODE_IP}}:{{K8S_API_SERCURE_PORT}}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
mon
  • 18,789
  • 22
  • 112
  • 205
  • 1
    Thanks bro. This solution help me to solve my problem with mysql master slave replication as mentioned in https://stackoverflow.com/questions/48045100/how-to-use-return-values-of-one-task-in-another-task-for-a-different-host-in-ans – Jerald Sabu M Jan 01 '18 at 16:44
  • 1
    Thanks as well. This is a workaround to really pass variables between different plays on different hosts. – schmichri Jun 28 '18 at 15:22
  • 1
    Top answer. This worked for me and this is the technically correct way to pass variables among different plays. – NIK Sep 11 '18 at 18:39
  • I do exactly this. You do run these plays separately or in one go? – zenin Apr 04 '19 at 14:29
  • 1
    @zenin, the same dummy host variable needs to exist for both playbooks (like a memory shared both the worker playbook and master playbook). If run separately, it cannot be shared. – mon Apr 14 '19 at 00:09
  • yea figured it out as well to just run the multiple playbooks using the include feature. to be able to run it seperately yiou can still save the output in a file locally. – zenin May 01 '19 at 08:58
  • 2
    This is also very valuable when running multiple plays (on the same playbook) where you want to share a variable from a play with a hostgroup, e.g. `hosts: staging,production` – jneuendorf Jun 17 '19 at 11:27
  • Thank you! This was exactly what I was looking for, saved me some time. – alexdotsh Jun 10 '20 at 19:09
  • How do you prevent ansible from spitting out error about unable to gather facts for 'K8S_TOKEN_HOLDER' since that is not a valid host? – TSG Sep 11 '21 at 15:27
  • @TSG. To start with I'm not a big fan of that "dummy host" thing which only adds complexity IMO. I would just register the vars on the k8smaster or on localhost directly and use them from there through `hostvars`... Meanwhile, if you want to use that, why would run anything on that dummy host anyway? Just don't use it as a target. If your playbook targets `all`, use an other group. – Zeitounator Sep 12 '21 at 08:25
  • Unfortunately it doesn't work when there is a `ansible.builtin.meta: refresh_inventory` between plays – Paweł Feb 16 '22 at 15:18
  • Isn't there yet any cleaner solution? – Mohammed Noureldin Oct 18 '22 at 23:58
21

I have had similar issues with even the same host, but across different plays. The thing to remember is that facts, not variables, are the persistent things across plays. Here is how I get around the problem.

#!/usr/local/bin/ansible-playbook --inventory=./inventories/ec2.py
---
- name: "TearDown Infrastructure !!!!!!!"
  hosts: localhost
  gather_facts: no
  vars:
    aws_state: absent
  vars_prompt:
    - name: "aws_region"
      prompt: "Enter AWS Region:"
      default: 'eu-west-2'
  tasks:
    - name: Make vars persistant
      set_fact:
        aws_region: "{{aws_region}}"
        aws_state: "{{aws_state}}"




- name: "TearDown Infrastructure hosts !!!!!!!"
  hosts: monitoring.ec2
  connection: local
  gather_facts: no
  tasks:
    - name: set the facts per host
      set_fact:
        aws_region: "{{hostvars['localhost']['aws_region']}}"
        aws_state: "{{hostvars['localhost']['aws_state']}}"


    - debug:
        msg="state {{aws_state}} region {{aws_region}} id {{ ec2_id }} "

- name: last few bits
  hosts: localhost
  gather_facts: no
  tasks:
    - debug:
        msg="state {{aws_state}} region {{aws_region}} "

results in

Enter AWS Region: [eu-west-2]:


PLAY [TearDown Infrastructure !!!!!!!] ***************************************************************************************************************************************************************************************************

TASK [Make vars persistant] **************************************************************************************************************************************************************************************************************
ok: [localhost]

PLAY [TearDown Infrastructure hosts !!!!!!!] *********************************************************************************************************************************************************************************************

TASK [set the facts per host] ************************************************************************************************************************************************************************************************************
ok: [XXXXXXXXXXXXXXXXX]

TASK [debug] *****************************************************************************************************************************************************************************************************************************
ok: [XXXXXXXXXXX] => {
    "changed": false,
    "msg": "state absent region eu-west-2 id i-0XXXXX1 "
}

PLAY [last few bits] *********************************************************************************************************************************************************************************************************************


TASK [debug] *****************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "changed": false,
    "msg": "state absent region eu-west-2 "
}

PLAY RECAP *******************************************************************************************************************************************************************************************************************************
XXXXXXXXXXXXX              : ok=2    changed=0    unreachable=0    failed=0
localhost                  : ok=2    changed=0    unreachable=0    failed=0
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
krad
  • 1,321
  • 1
  • 15
  • 12
  • *"facts not variables [are] persistent across plays"* - are you certain of this? –  Jan 28 '18 at 07:36
  • 3
    Yes as long as all the plays are in the same invocation of ansible. They can obviously be overwritten if you do multiple gather facts – krad Feb 06 '18 at 15:56
  • does not work. running two tasks, with when: ansible_host in groups['foo'] and setting a fact, then trying to access this fact from the next task, which is applied to another group fails – dyasny Oct 08 '19 at 20:52
  • Difficult to comment on without example. Your set theory maybe wrong, and there has been a fair few version bumps in ansible over the last 2.5 years – krad Oct 09 '19 at 21:29
4

You can use an Ansible known behaviour. That is using group_vars folder to load some variables at your playbook. This is intended to be used together with inventory groups, but it is still a reference to the global variable declaration. If you put a file or folder in there with the same name as the group, you want some variable to be present, Ansible will make sure it happens!

As for example, let's create a file called all and put a timestamp variable there. Then, whenever you need, you can call that variable, which will be available to every host declared on any play inside your playbook.

I usually do this to update a timestamp once at the first play and use the value to write files and folders using the same timestamp.

I'm using lineinfile module to change the line starting with timestamp :

Check if it fits for your purpose.

On your group_vars/all

timestamp: t26032021165953

On the playbook, in the first play: hosts: localhost gather_facts: no

- name: Set timestamp on group_vars
  lineinfile:
    path: "{{ playbook_dir }}/group_vars/all"
    insertafter: EOF
    regexp: '^timestamp:'
    line: "timestamp: t{{ lookup('pipe','date +%d%m%Y%H%M%S') }}"
    state: present

On the playbook, in the second play:

hosts: any_hosts
gather_facts: no

tasks:
  - name: Check if timestamp is there
    debug:
      msg: "{{ timestamp }}"
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
guistela
  • 104
  • 4
1

HOW

  1. post_tasks: save variables to file
  2. pre_tasks: include saved variable file
- name: PLAYBOOK A
  hosts: localhost
  connection: local
  gather_facts: no
  post_tasks:
    - no_log: true
      set_fact:
        MY_VARS: {}

    - no_log: true
      set_fact:
        MY_VARS: "{{ MY_VARS | combine({ item.key: item.value }) }}"
      loop: "{{ vars | dict2items }}"
      # only store variable name start with "P_"
      when: item.key.startswith('P_')

    - no_log: true
      file:
        path: my_vars
        state: directory

    - no_log: true
      copy:
        content: "{{ MY_VARS }}"
        dest: my_vars/my_vars.json
  tasks:
    - set_fact:
        P_HELLO: WOLRD
        P_BOOLEAN: false
        NO_STORE: aaa

- name: PLAYBOOK B
  hosts: all
  gather_facts: no
  pre_tasks:
    - no_log: true
      include_vars:
        dir: my_vars
        extensions:
        - json
  tasks:
    - debug:
        var: P_HELLO

    - debug:
        var: P_BOOLEAN

    - debug:
        var: NO_STORE

    # overwrite
    - set_fact:
        P_HELLO: hahaha

    - debug:
        var: P_HELLO

RESULT

PLAY [PLAYBOOK A] **************************************************************************************************************************************

TASK [set_fact] ****************************************************************************************************************************************
ok: [localhost]

TASK [set_fact] ****************************************************************************************************************************************
ok: [localhost]

TASK [set_fact] ****************************************************************************************************************************************
ok: [localhost] => (item=None)
ok: [localhost] => (item=None)
ok: [localhost]

TASK [file] ********************************************************************************************************************************************
ok: [localhost]

TASK [copy] ********************************************************************************************************************************************
ok: [localhost]

PLAY [PLAYBOOK B] **************************************************************************************************************************************

TASK [include_vars] ************************************************************************************************************************************
ok: [demohost]

TASK [debug] *******************************************************************************************************************************************
ok: [demohost] => {
    "P_HELLO": "WOLRD"
}

TASK [debug] *******************************************************************************************************************************************
ok: [demohost] => {
    "P_BOOLEAN": false
}

TASK [debug] *******************************************************************************************************************************************
ok: [demohost] => {
    "NO_STORE": "VARIABLE IS NOT DEFINED!"
}

TASK [set_fact] ****************************************************************************************************************************************
ok: [demohost]

TASK [debug] *******************************************************************************************************************************************
ok: [demohost] => {
    "P_HELLO": "hahaha"
}

PLAY RECAP *********************************************************************************************************************************************
demohost                   : ok=6    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
localhost                  : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0