Ansible - unable to append correctly using the file plugin

Viewed 59

I'm trying to use the Ansible file plugin to append data to a txt file.

This is my playbook:

- name: Setting some variables.
  hosts: "{{ target }}"
  gather_facts: no
  connection: local
  
  tasks:
    - name: Delete newDevices file.
      file:
        path: newDevices.txt
        state: absent
      run_once: true

    - name: Create newDevices file.
      file:
        path: newDevices.txt
        state: touch
      run_once: true

    - name: Add new devices to file.
      delegate_to: 127.0.0.1
      lineinfile:
        insertafter: EOF
        path: newDevices.txt
        line: "{{ inventory_hostname }}"

This is my host file:

[lab]
routerA01.mgt.net
routerA02.mgt.net
routerB01.mgt.net
routerB02.mgt.net

This is how I'm running the playbook:

ansible-playbook myplaybook.yml -i hosts --limit "lab[0-1]" -e target=lab

The newDevices.txt file gets deleted and recreated but the contents of the file vary from having 1 hostname or 2 hostnames. If I change the limit in the run command to --limit "lab[0-2]" then the same thing happens. Sometimes 2 hostnames will print and other times all 3 will print. Seems like there will always be a time when the newDevices.txt file will contain one less hostname in it.

Not sure why this is happening. Tried adding a pause after the file is created to maybe give the processing a little extra time, but that didn't help either.

2 Answers

There is a couple of improvements

  1. Put all tasks into a block delegated to localhost. All of them should run only once on the localhost anyway.

  2. Then, there is no point to declare global connection: local

  3. Set hosts: all if you plan to limit the inventory on the command line. In this case, there is no point to put the group into a variable.

  4. Write the file in a loop. This avoids concurrent writing from multiple instances. (What is probably the main reason for your troubles.)

Given the (simplified) inventory

shell> cat hosts
[lab]
A01
A02
B01
B02

The playbook

shell> cat pb.yml
- hosts: all
  gather_facts: false
  
  tasks:
    - block:
        - file:
            path: newDevices.txt
            state: absent
        - file:
            path: newDevices.txt
            state: touch
        - lineinfile:
            insertafter: EOF
            path: newDevices.txt
            line: "{{ item }}"
          loop: "{{ ansible_play_hosts }}"
      delegate_to: localhost
      run_once: true

gives

shell> ansible-playbook pb.yml --limit lab[0:1]

PLAY [all] **************************************************************************************

TASK [file] *************************************************************************************
changed: [A01 -> localhost]

TASK [file] *************************************************************************************
changed: [A01 -> localhost]

TASK [lineinfile] *******************************************************************************
changed: [A01 -> localhost] => (item=A01)
changed: [A01 -> localhost] => (item=A02)

PLAY RECAP **************************************************************************************
A01: ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
shell> cat newDevices.txt 
A01
A02

  • The above playbook is not idempotent. The file is always recreated. The result is that the files will always keep the hosts from the last play only.

  • The simplified playbook below is idempotent. The list of the hosts in the file, however, will be cumulative

shell> cat pb.yml
- hosts: all
  gather_facts: false

  tasks:
    - lineinfile:
        create: true
        insertafter: EOF
        path: newDevices.txt
        line: "{{ item }}"
      loop: "{{ ansible_play_hosts }}"
      delegate_to: localhost
      run_once: true

gives

shell> ansible-playbook pb.yml --limit lab[0:1]

PLAY [all] **************************************************************************************

TASK [lineinfile] *******************************************************************************
changed: [A01 -> localhost] => (item=A01)
changed: [A01 -> localhost] => (item=A02)

PLAY RECAP **************************************************************************************
A01: ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
shell> cat newDevices.txt 
A01
A02
shell> ansible-playbook pb.yml --limit lab[0:1]

PLAY [all] **************************************************************************************

TASK [lineinfile] *******************************************************************************
ok: [A01 -> localhost] => (item=A01)
ok: [A01 -> localhost] => (item=A02)

PLAY RECAP **************************************************************************************
A01: ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
shell> ansible-playbook pb.yml --limit lab[0:2]

PLAY [all] **************************************************************************************

TASK [lineinfile] *******************************************************************************
ok: [A01 -> localhost] => (item=A01)
ok: [A01 -> localhost] => (item=A02)
changed: [A01 -> localhost] => (item=B01)

PLAY RECAP **************************************************************************************
A01: ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
shell> cat newDevices.txt 
A01
A02
B01
shell> ansible-playbook pb.yml --limit lab[0]

PLAY [all] **************************************************************************************

TASK [lineinfile] *******************************************************************************
ok: [A01 -> localhost] => (item=A01)

PLAY RECAP **************************************************************************************
A01: ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
shell> cat newDevices.txt 
A01
A02
B01
  • The playbook below is idempotent and the file will keep the list of hosts from the last play only
shell> cat pb.yml
- hosts: all
  gather_facts: false
  
  tasks:
    - copy:
        dest: newDevices.txt
        content: |-
          {% for host in ansible_play_hosts %}
          {{ host }}
          {% endfor %}
      delegate_to: localhost
      run_once: true

Another way to do this would be to have a play targeting localhost itself since the file is to be created on the controller anyway. The file can be created using the "items" of lab inventory group.

Something like below:

- hosts: localhost
  connection: local

  tasks:
    - file:
        path: newDevices.txt
        state: absent

    - lineinfile:
        path: newDevices.txt
        create: yes
        insertafter: EOF
        line: "{{ item }}"
      loop: "{{ groups['lab'][0:2] }}"

In the above example, the file gets created by lineinfile task itself. You can also remove/change the list slice of [0:2] as per your requirement.

Then run it with:

ansible-playbook myplaybook.yml -i hosts
Related