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?

Kevin
  • 827
  • 3
  • 13
  • 23

8 Answers8

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 }}"
Basil A
  • 2,060
  • 3
  • 18
  • 18
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 }}"
gbolo
  • 623
  • 5
  • 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 '17 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 '17 at 14:24
  • Using ansible 2.2. Inventory file is super basic right now. [app] app1.us.com ansible_host=192.168.1.1 app2.us.com ansible_host=192.168.1.2 app3.us.com ansible_host=192.168.1.3 – Kevin Feb 16 '17 at 16:17
  • for me following worked (ansible_host was not recognized by Ansible) line: "{{ hostvars[item].ansible_ssh_host }} {{ hostvars[item].inventory_hostname }} {{ hostvars[item].inventory_hostname_short }}" – smishra Nov 04 '17 at 20:06
  • 1
    had to add following: `unsafe_writes: true` otherwise got error on docker-provisioned hosts – Peter Butkovic Apr 29 '18 at 19:40
  • 1
    This answer does not take into account when the ip address changes. – Basil A Oct 15 '18 at 11:09
  • 2
    on `regexp: '.*{{ item }}$'`, what if we have two hosts named `s1` and `ss1`? – ahmadali shafiee Aug 06 '19 at 06: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 :-).
Vipul HK
  • 71
  • 1
  • 1
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') }}"
ahmadali shafiee
  • 138
  • 1
  • 2
  • 12
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.

arkantos
  • 111
  • 3
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']}}"
SaravAK
  • 11
  • 1
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.

usersina
  • 101
  • 2
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.

danmichaelo
  • 602
  • 1
  • 5
  • 8