Search code examples
linuxansibleyamljinja2

How can I pass a varialbe set by the set_fact module to the Jinja2 template?


I have a role to setup NATS cluster,, I've used host_vars to define which node is the master node like below:

is_master: true

Then in the setup-nats.yml task, I used the following to extract the master node's IP address based on the host_var I've set and then used it as a variable for the Jinja2 template, however, the variable doesn't get passed down to the template and I get the `variable 'master_ip' is undefined.

- name: Set master IP
  set_fact:
    set_master_ip: "{{ ansible_facts['default_ipv4']['address'] }}"
    cacheable: yes
  when: is_master

- name: debug
  debug:
    msg: "{{ set_master_ip }}"
  run_once: true

- name: generate nats-server.conf for the slave nodes
  template:
    src: nats-server-slave.conf.j2
    dest: /etc/nats-server.conf
    owner: nats
    group: nats
    mode: 0644
  when:
    - is_master == false
  vars:
    master_ip: "{{ set_master_ip }}"
  notify: nats-server

The variable is used like below in the Jinja2 template:

routes = [
    nats-route://ruser:{{ nats_server_password }}@{{ master_ip }}:6222
  ]
}

Questions:

  • Is this approach according to the best practices?
  • What is the correct way of doing the above so the variable is passed down to the template?

Test Output:

I'm using Molecule to test my Ansible and even though in the debug task the IP address is visible, it doesn't get passed down to the template:

TASK [nats : Set master IP] ****************************************************
ok: [target1]
skipping: [target2]
skipping: [target3]

TASK [nats : debug] ************************************************************
ok: [target1] => 
  msg: 10.0.2.15

TASK [nats : generate nats-server.conf for the slave nodes] ********************
skipping: [target1]
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.errors.AnsibleUndefinedVariable: {{ set_master_ip }}: 'set_master_ip' is undefined
fatal: [target2]: FAILED! => changed=false 
  msg: 'AnsibleUndefinedVariable: {{ set_master_ip }}: ''set_master_ip'' is undefined'
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.errors.AnsibleUndefinedVariable: {{ set_master_ip }}: 'set_master_ip' is undefined
fatal: [target3]: FAILED! => changed=false 
  msg: 'AnsibleUndefinedVariable: {{ set_master_ip }}: ''set_master_ip'' is undefined'

Any help is appreciated, thanks in advance.

UPDATE: I suspect the issue has something to do with the variable scope being in the host context but cannot find a way to fix it ( I might be wrong though).


Solution

  • Far from being best practice IMO but answering your direct question. Your problem is not passing the variable to your template but the fact it is not assigned to all hosts in your play loop (and hence is undefined on any non master node). The following (untested) addresses that issue keeping the same task structure.

    - name: Set master IP for all nodes
      ansible.builtin.set_fact:
        master_ip: "{{ hostvars | dict2items | map(attribute='value'
          | selectattr('is_master', 'defined') | selectattr('is_master')
          | map(attribute='ansible_facts.default_ipv4.address') | first }}"
        cacheable: yes
      run_once: true
    
    - name: Show calculated master IP (making sure it is assigned everywhere)
      ansible.builtin.debug:
        msg: "{{ master_ip }}"
    
    - name: generate nats-server.conf for the slave nodes
      ansible.builtin.template:
        src: nats-server-slave.conf.j2
        dest: /etc/nats-server.conf
        owner: nats
        group: nats
        mode: 0644
      when: not is_master | bool
      notify: nats-server
    

    Ideas for enhancement (non exhaustive):

    • Select your master based on a group membership in the inventory rather than on a host attribute. This makes gathering the ip easier (e.g. master_ip: "{{ hostvars[groups.master | first].ansible_facts.default_ipv4.address }}"
    • Set the ip as a play var or directly inside the inventory for the node group rather than in a set_fact task.