1
votes

Using Ansible 2.9.12

Question: How do I configure Ansible to ensure the contents of a file is equal amongst at least 3 hosts, when the file is present at at least one host?

Imagine there are 3 hosts.

Host 1 does not has /file.txt.

Host 2 has /file.txt with contents hello.

Host 3 has /file.txt with contents hello.

Before the play is run, I am unaware whether the file is present or not. So the file could exist on host1, or host2 or host3. But the file exists on at least one of the hosts.

How would I ensure each time Ansible runs, the files across the hosts are equal. So in the end, Host 1 has the same file with the same contents as Host 2 or Host 3.

I'd like this to be dynamically set, instead of specifying the host names or group names, e.g. when: inventory_hostname == host1.

I am not expecting a check to see whether the contents of host 2 and 3 are equal

I do however, want this to be setup in an idempotent fashion.

3
Is it something different to what the copy module can do?seshadri_c
I only want to use the module when at a hosts, the file is not present. Before the play is run, I am unaware whether the file is present or not.Kevin C
so the file if exists on any hosts lets say 3 then you need to copy the file from 3 to host 2 and host 1, right ? If yes, you can use rsync docs.ansible.com/ansible/latest/modules/synchronize_module.html and delegate_toerror404
but beforehand I don't know on which machine the file exists. It could exist on either 1, 2 or 3.Kevin C

3 Answers

1
votes

The play below does the job, I think

shell> cat pb.yml
- hosts: all
  tasks:
    - name: Get status.
      stat:
        path: /file.txt
      register: status
    - block:
        - name: Create dictionary status.
          set_fact:
            status: "{{ dict(keys|zip(values)) }}"
          vars:
            keys: "{{ ansible_play_hosts }}"
            values: "{{ ansible_play_hosts|
                        map('extract', hostvars, ['status','stat','exists'])|
                        list }}"
        - name: Fail. No file exists.
          fail:
            msg: No file exists
          when: status.values()|list is not any
        - name: Set reference to first host with file present.
          set_fact:
            reference: "{{ status|dict2items|
                           selectattr('value')|
                           map(attribute='key')|
                           first }}"
        - name: Fetch file.
          fetch:
            src: /file.txt
            dest: /tmp
          delegate_to: "{{ reference }}"
      run_once: true
    - name: Copy file if not exist
      copy:
        src: "/tmp/{{ reference }}/file.txt"
        dest: /file.txt
      when: not status[inventory_hostname]

But, this doesn't check the existing files are in sync. It would be safer to sync all hosts, I think

    - name: Synchronize file
      synchronize:
        src: "/tmp/{{ reference }}/file.txt"
        dest: /file.txt
      when: not status[inventory_hostname]

Q: "FATAL. could not find or access '/tmp/test-multi-01/file.txt on the Ansible controller. However, folder /tmp/test-multi-03 is present with the file.txt in it."

A: There is a problem with the fetch module when the task is delegated to another host. When the TASK [Fetch file.] is delegated to test-multi-01 which is localhost in this case changed: [test-multi-03 -> 127.0.0.1] the file will be fetched from test-multi-01 but will be stored in /tmp/test-multi-03/file.txt. The conclusion is, the fetch module ignores delegate_to when it comes to creating host-specific directories (not reported yet).

As a workaround, it's possible to set flat: true and store the files in a specific directory. For example, add the variable sync_files_dir with the directory, set fetch flat: true, and use the directory to both fetch and copy the file

- hosts: all
  vars:
    sync_files_dir: /tmp/sync_files
  tasks:
    - name: Get status.
      stat:
        path: /file.txt
      register: status
    - block:
        - name: Create dir for files to be fetched and synced
          file:
            state: directory
            path: "{{ sync_files_dir }}"
          delegate_to: localhost
        - name: Create dictionary status.
          set_fact:
            status: "{{ dict(keys|zip(values)) }}"
          vars:
            keys: "{{ ansible_play_hosts }}"
            values: "{{ ansible_play_hosts|
                        map('extract', hostvars, ['status','stat','exists'])|
                        list }}"
        - debug:
            var: status
        - name: Fail. No file exists.
          fail:
            msg: No file exists
          when: status.values()|list is not any
        - name: Set reference to first host with file present.
          set_fact:
            reference: "{{ status|dict2items|
                           selectattr('value')|
                           map(attribute='key')|
                           first }}"
        - name: Fetch file.
          fetch:
            src: /file.txt
            dest: "{{ sync_files_dir }}/"
            flat: true
          delegate_to: "{{ reference }}"
      run_once: true
    - name: Copy file if not exist
      copy:
        src: "{{ sync_files_dir }}/file.txt"
        dest: /file.txt
      when: not status[inventory_hostname]
0
votes

We can achieve it by fetching the file from hosts where the file exists. The file(s) will be available on the control machine. However if the file which will be the source, exists on more than 1 node, then there will be no single source of truth.

Consider an inventory:

[my_hosts]
host1
host2
host3

Then the below play can fetch the file, then use that file to copy to all nodes.

# Fetch the file from remote host if it exists
- hosts: my_hosts

  tasks:
  - stat:
      path: /file.txt
    register: my_file
  - fetch:
      src: /file.txt
      dest: /tmp/
    when: my_file.stat.exists
  - find:
      paths:
      - /tmp
      patterns: file.txt
      recurse: yes
    register: local_file
    delegate_to: localhost
  - copy:
      src: "{{ local_file.files[0].path }}"
      dest: /tmp

If multiple hosts had this file then it would be in /tmp/{{ ansible_host }}. Then as we won't have a single source of truth, our best estimate can be to use the first file and apply on all hosts.

-1
votes

Well i believe the get_url module is pretty versatile - allows for local file paths or paths from a web server. Try it and let me know.

 - name: Download files in all host
   hosts: all 
   tasks:
    - name: Download file from a file path
      get_url:
        url: file:///tmp/file.txt
        dest: /tmp/

Edited ans: (From documentation: For the synchronize module, the “local host” is the host the synchronize task originates on, and the “destination host” is the host synchronize is connecting to)

  - name: Check that the file exists
    stat:
      path: /etc/file.txt
    register: stat_result

  - name: copy the file to other hosts by delegating the task to the source host
    synchronize:
      src: path/host
      dest: path/host
    delegate_to: my_source_host
    when: stat_result.stat.exists