18

I am deploying a small 3 node cluster and I want to add the public IP addresses as defined in my inventory to the /etc/hosts files of all of the nodes.

I am trying to use the following, but it is giving me an error:

- name: Add IP address of all hosts to all hosts
  lineinfile: 
    dest: /etc/hosts
    line: '{{ hostvars[item]["ansible_host"] }} {{ hostvars[item]["ansible_hostname"] }} {{ hostvars[item]["ansible_nodename"] }}'
    state: present
  with_items: groups['all']

The error is:

fatal: [app1.domain.com]: FAILED! => {"failed": true, "msg": "the field 'args' has an invalid value, which appears to include a variable that is undefined. The error was: 'ansible.vars.hostvars.HostVars object' has no attribute u"groups['all']"\n\nThe error appears to have been in '/Users/k/Projects/Ansible/roles/common/tasks/main.yml': line 29, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- name: Add IP address of all hosts to all hosts\n ^ here\n"}

Any ideas on what I'm missing?

8 Answers 8

31

The previous answer simply does not work because it adds a new line for the same host instead of modifying the existing line when an IP Address for a host changes.

The following solution takes into account when the ip address changes for a specific server and handles it well by only modifying the line instead of adding duplicate entries.

---
- name: Add IP address of all hosts to all hosts
  lineinfile:
    dest: /etc/hosts
    regexp: '.*{{ item }}$'
    line: "{{ hostvars[item].ansible_host }} {{item}}"
    state: present
  when: hostvars[item].ansible_host is defined
  with_items: "{{ groups.all }}"
1
  • 1
    Hi Basil, this is a better solution. I haven't considered IP changing. Thanks
    – gbolo
    Oct 25, 2018 at 14:31
17

Looks like you have errors in your syntax. Also what version of ansible are you using? The variable names may be different. On version 2.2 this works for me:

- name: Add IP address of all hosts to all hosts
  lineinfile:
    dest: /etc/hosts
    line: "{{ hostvars[item].ansible_host }} {{ hostvars[item].inventory_hostname }} {{ hostvars[item].inventory_hostname_short }}"
    state: present
  with_items: "{{ groups.all }}"

UPDATE

Basil has thought about situations when the IP changes. In that case it is better to use his suggested solution:

- name: Add IP address of all hosts to all hosts
  lineinfile:
    dest: /etc/hosts
    regexp: '.*{{ item }}$'
    line: "{{ hostvars[item].ansible_host }} {{item}}"
    state: present
  when: hostvars[item].ansible_host is defined
  with_items: "{{ groups.all }}"
7
  • My problem was needing quotes around the groups['all'] / groups.all section. Every example I'd found online didn't have quotes around it, but that was definitely my issue. Thanks
    – Kevin
    Feb 16, 2017 at 13:22
  • yea when using variables you need double quotes (except in when clause). Out of curiosity, what version of ansible are you using? also what does your inventory file look like?
    – gbolo
    Feb 16, 2017 at 14:24
  • 1
    had to add following: unsafe_writes: true otherwise got error on docker-provisioned hosts Apr 29, 2018 at 19:40
  • 1
    This answer does not take into account when the ip address changes.
    – Basil A
    Oct 15, 2018 at 11:09
  • 2
    on regexp: '.*{{ item }}$', what if we have two hosts named s1 and ss1? Aug 6, 2019 at 6:00
7

I had the same issue and here is my solution for anyone who is interested.

hosts/dev.ini

[controller]
controller1 ansible_ssh_host=10.11.11.10
controller2 ansible_ssh_host=10.11.11.11
controller3 ansible_ssh_host=10.11.11.12

[compute]
compute1 ansible_ssh_host=10.11.11.13
compute2 ansible_ssh_host=10.11.11.14
compute3 ansible_ssh_host=10.11.11.15

[block]
block1 ansible_ssh_host=10.11.11.16
block2 ansible_ssh_host=10.11.11.17

[haproxy]
haproxy1 ansible_ssh_host=10.11.11.18

[nginx]
nginx1 ansible_ssh_host=10.11.11.19

[deployment]
deployment ansible_ssh_host=10.11.11.20

[all:vars]
ansible_python_interpreter=/usr/bin/python3

tasks/main.yml

---
- name: Update /etc/hosts
  become: true
  blockinfile:
      path: /etc/hosts
      create: yes
      block: |
        127.0.0.1 localhost

        # The following lines are desirable for IPv6 capable hosts
        ::1 ip6-localhost ip6-loopback
        fe00::0 ip6-localnet
        ff00::0 ip6-mcastprefix
        ff02::1 ip6-allnodes
        ff02::2 ip6-allrouters
        ff02::3 ip6-allhosts

        {% for item in ansible_play_batch %}
        {{ hostvars[item].ansible_ssh_host }}   {{ item }}    
        {% endfor %}

Notes:

  • python 3.7.5 ansible 2.9.0
  • I decided to go with blockinfile instead of using templates because hostvars context was not getting updated inside the template. Plus I was in a hurry :-).
2

I've combined gbolo's solution and this solution with addition to add local hostname to /etc/hosts and also make sure not to mistake hostnames like s1 and ss1 with each other

- name: add hostname to /etc/hosts
  vars:
    comment: '# added by ansible'
  lineinfile:
    dest: /etc/hosts
    regexp: "127[.]0[.]0[.]1.*"
    line: "127.0.0.1 localhost.localdomain localhost {{ ansible_hostname }} {{ comment }}"
    state: present
    backup: yes
- name: add IP address of all hosts to /etc/hosts
  vars:
    comment: '# added by ansible'
  lineinfile:
    dest: /etc/hosts
    regexp: ".* {{ item }} {{ comment }}"
    line: "{{ hostvars[item]['ansible_env'].SSH_CONNECTION.split(' ')[2] }} {{ item }} {{ comment }}"
    state: present
    backup: yes
  when: ansible_hostname != item
  loop: "{{ query('inventory_hostnames', 'all') }}"
1

In Ansible2.5 this is a working solution and I have used ansible_env.SSH_CONNECTION variable to get the remote server ip and ansible_host is not defined in the gathered facts.

- name: Update the /etc/hosts file with node name
      tags: etchostsupdate
      become: yes
      become_user: root
      lineinfile:
        path: "/etc/hosts"
        regexp: "{{ hostvars[item]['ansible_env'].SSH_CONNECTION.split(' ')[2] }}\t{{ hostvars[item]['ansible_hostname']}}\t{{ hostvars[item]['ansible_hostname']}}"
        line: "{{ hostvars[item]['ansible_env'].SSH_CONNECTION.split(' ')[2] }}\t{{ hostvars[item]['ansible_hostname']}}\t{{ hostvars[item]['ansible_hostname']}}"
        state: present
        backup: yes
      register: etchostsupdate
      when: ansible_hostname != "{{ item }}"
      with_items: "{{groups['app']}}"
2
  • why are you duplicating hostvars[item]['ansible_hostname']? Aug 6, 2019 at 7:55
  • also you need to remove hostvars[item]['ansible_env'].SSH_CONNECTION.split(' ')[2] from regexp section in order to handle ip changes Aug 6, 2019 at 7:56
1

An addition to all the other great answers:

---
- name: test
  hosts: all
  tasks:
  - name: generate file
    blockinfile:
      backup: yes
      path: /etc/hosts
      block: |
        {% for host in groups['all'] %} 
        {{ hostvars[host]['ansible_facts']['eth1']['ipv4']['address'] }} {{ hostvars[host]['ansible_facts']['fqdn'] }} {{ hostvars[host]['ansible_facts']['hostname'] }} 
        {% endfor %}

You should change the nic name in hostvars[host]['ansible_facts']['eth1']['ipv4']['address'] according to your available nics. Also this is valid assuming your nic names are uniformly distributed accross your environment.

0

The solution from Basil A is quite flexible, but it can leave redundant entries if you make hostname changes or remove hosts from inventory. For this reason, I created a slightly refined version of the solution by Vipul HK, using blockinfile instead of lineinfile:

- name: Add Ansible inventory mappings to /etc/hosts
  become: true
  blockinfile:
    path: /etc/hosts
    block: |
      {% for host in groups['all'] %}
      {{ hostvars[host].ansible_host }} {{ host }}
      {% endfor %}

This solution ensures that the host mappings in /etc/hosts always matches the ones in the inventory file.

0

Building on @arkantos' answer. If you have multiple network interfaces and want to fine tune which IPs you add, then you should do the following:

In your inventory file, further add a hv_net_ife for the interface you want to use. I also add for hv_hostname since I use it elsewhere to set the hostname of the machine.

[master]
xxx.xxx.xxx.xxx hv_hostname="controller-node" hv_net_ife="enp7s0"

[node]
xxx.xxx.xxx.xxx hv_hostname="staging-node" hv_net_ife="ens10"
xxx.xxx.xxx.xxx hv_hostname="prod-node" hv_net_ife="ens10"
- name: Add the IP addresses of all hosts to each other
  ansible.builtin.lineinfile:
    dest: '/etc/hosts'
    regexp: ".*\t{{ hostvars[item]['ansible_hostname'] }}\t{{ hostvars[item]['ansible_hostname'] }}"
    line: "{{ hostvars[item]['ansible_' + hv_net_ife]['ipv4']['address'] }}\t{{ hostvars[item]['hv_hostname'] }}"
    state: present
    backup: true
  when:
    - hostvars[item]['ansible_' + hv_net_ife] is defined
    - hostvars[item]['ansible_hostname'] != hv_hostname
  with_items: '{{ groups.all }}'

For example, this was very useful since I created two virtual machines as part of the same private network (they both use the NIC ens10) but then I added another one. My cloud provider decided to give it enp7s0 so I had to scratch my head a little to get it to work.

Oh also you need the when condition - hostvars[item]['ansible_' + hv_net_ife] is defined else you will get an error since Ansible tries to look for all interfaces.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .