I have this
- name: cp images
copy:
src: "{{ item.src }} "
dest: "{{ item.dest }}"
owner: root
group: root
mode: u=rw, g=rw, o=r
loop:
- { src: images/Juniper/VMX/vmx.18.2/images/virtioa.qcow2, dest: /opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP/virtioa.qcow2}
- { src: images/Juniper/VMX/vmx.18.2/images/virtiob.qcow2, dest: /opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP/virtiob.qcow2}
- { src: images/Juniper/VMX/vmx.18.2/images/virtioc.qcow2, dest: /opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP/virtioc.qcow2}
- { src: images/Juniper/VMX/vmx.18.2/images/vFPC-20180605.img, dest: /opt/unetlab/addons/qemu/vmxvfp-18.2R1.8-domestic-VFP/virtioa.qcow2}
- { src: images/Juniper/VSRX/junos-vsrx3-x86-64-20.4R1.12.qcow2, dest: /opt/unetlab/addons/qemu/vsrxng-20.4R1.12/virtioa.qcow2}
- { src: images/Juniper/QFX/vqfx-20.2R1-2019010209-pfe-qemu.qcow, dest: /opt/unetlab/addons/qemu/vqfxpfe-10K-F-20.2R1.10/hda.qcow}
- { src: images/Juniper/QFX/vqfx-20.2R1.10-re-qemu.qcow2, dest: /opt/unetlab/addons/qemu/vqfxre-10K-F-20.2R1.10/hda.qcow2}
- { src: images/Cisco/L2/vios_l2-adventerprisek9-m.SSA.high_iron_20180619.qcow2, dest: /opt/unetlab/addons/qemu/viosl2-adventerprisek9-m.03.2017/virtioa.qcow2}
- { src: images/Cisco/L3/vios-adventerprisek9-m.vmdk.SPA.157-3.M3, dest: /opt/unetlab/addons/qemu/vios-adventerprisek9-m.SPA.156-1.T/vios-adventerprisek9-m.SPA.156-1.T.vmdk}
I'm trying to use Ansible to auto set up EVE-NG
I'm getting this error below
\n\t/home/ablake/Documents/Documents/personal-git/ansible/main/images/Juniper/VMX/vmx.18.2/images/virtiob.qcow2
on the Ansible Controller.\nIf you are using a module and expect the file to exist on the remote, see the remote_src option"}
Now I'm not 100% sure what I'm doing wrong, I see in the error log its got \n\t in front of the dir I have no idea where this is coming from, is this the problem? I don't know if it is or its just ansible debug
now without the loop below
- name: copy PFE to EVE-NG
tags: QFX
copy:
src: images/Juniper/QFX/vqfx-20.2R1-2019010209-pfe-qemu.qcow
dest: /opt/unetlab/addons/qemu/vqfxpfe-10K-F-20.2R1.10/hda.qcow
that works I maybe missing something super basic with loops but I don't understand the difference and I'm a little stuck sorry
Also tried with quotes below
- name: cp images
copy:
src: "{{ item.src }} "
dest: "{{ item.dest }}"
loop:
- { src: 'images/Juniper/VMX/vmx.18.2/images/virtioa.qcow2', dest: '/opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP/virtioa.qcow2'}
- { src: 'images/Juniper/VMX/vmx.18.2/images/virtiob.qcow2', dest: '/opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP/virtiob.qcow2'}
- { src: 'images/Juniper/VMX/vmx.18.2/images/virtioc.qcow2', dest: '/opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP/virtioc.qcow2'}
- { src: 'images/Juniper/VMX/vmx.18.2/images/vFPC-20180605.img', dest: '/opt/unetlab/addons/qemu/vmxvfp-18.2R1.8-domestic-VFP/virtioa.qcow2'}
- { src: 'images/Juniper/VSRX/junos-vsrx3-x86-64-20.4R1.12.qcow2', dest: '/opt/unetlab/addons/qemu/vsrxng-20.4R1.12/virtioa.qcow2'}
- { src: 'images/Juniper/QFX/vqfx-20.2R1-2019010209-pfe-qemu.qcow', dest: '/opt/unetlab/addons/qemu/vqfxpfe-10K-F-20.2R1.10/hda.qcow'}
- { src: 'images/Juniper/QFX/vqfx-20.2R1.10-re-qemu.qcow2', dest: '/opt/unetlab/addons/qemu/vqfxre-10K-F-20.2R1.10/hda.qcow2'}
- { src: 'images/Cisco/L2/vios_l2-adventerprisek9-m.SSA.high_iron_20180619.qcow2', dest: '/opt/unetlab/addons/qemu/viosl2-adventerprisek9-m.03.2017/virtioa.qcow2'}
- { src: 'images/Cisco/L3/vios-adventerprisek9-m.vmdk.SPA.157-3.M3', dest: '/opt/unetlab/addons/qemu/vios-adventerprisek9-m.SPA.156-1.T/vios-adventerprisek9-m.SPA.156-1.T.vmdk'}
same error below
"Could not find or access 'images/Juniper/VMX/vmx.18.2/images/vFPC-20180605.img '\nSearched in:\n\t/home/ablake/Documents/Documents/personal-git/ansible/main/files/images/Juniper/VMX/vmx.18.2/images/vFPC-20180605.img \n\t/home/ablake/Documents/Documents/personal-git/ansible/main/images/Juniper/VMX/vmx.18.2/images/vFPC-20180605.img \n\t/home/ablake/Documents/Documents/personal-git/ansible/main/files/images/Juniper/VMX/vmx.18.2/images/vFPC-20180605.img \n\t/home/ablake/Documents/Documents/personal-git/ansible/main/images/Juniper/VMX/vmx.18.2/images/vFPC-20180605.img on the Ansible Controller.\nIf you are using a module and expect the file to exist on the remote, see the remote_src option"}
The error says:
"Could not find or access 'images/... Searched in: ... main/files/images/ ... main/images/ ...".
The attribute src of the module copy says:
Local path to a file to copy to the remote server. This can be absolute or relative.
See Search paths in Ansible and the section Resolving local relative paths in particular. In your case:
When you specify a relative path for a local file, Ansible will try to find that file ... relative to the file in which the task is defined ... Specifically, Ansible tries to find the file ... in its appropriate subdirectory—“files”, ...
This explains why Ansible searched for the files in the directories
main/images/
main/files/images/
It's up to you to provide the correct path. For example, given the tree at the controller
shell> tree images/
images/
├── Cisco
│ ├── L2
│ │ └── vios_l2-adventerprisek9-m.SSA.high_iron_20180619.qcow2
│ └── L3
│ └── vios-adventerprisek9-m.vmdk.SPA.157-3.M3
└── Juniper
├── QFX
│ ├── vqfx-20.2R1.10-re-qemu.qcow2
│ └── vqfx-20.2R1-2019010209-pfe-qemu.qcow
├── VMX
│ └── vmx.18.2
│ └── images
│ ├── vFPC-20180605.img
│ ├── virtioa.qcow2
│ ├── virtiob.qcow2
│ └── virtioc.qcow2
└── VSRX
└── junos-vsrx3-x86-64-20.4R1.12.qcow2
and the tree at the remote host
shell> ssh admin#test_11 find /opt
/opt
/opt/unetlab
/opt/unetlab/addons
/opt/unetlab/addons/qemu
/opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP
/opt/unetlab/addons/qemu/viosl2-adventerprisek9-m.03.2017
/opt/unetlab/addons/qemu/vqfxpfe-10K-F-20.2R1.10
/opt/unetlab/addons/qemu/vsrxng-20.4R1.12
/opt/unetlab/addons/qemu/vios-adventerprisek9-m.SPA.156-1.T
/opt/unetlab/addons/qemu/vmxvfp-18.2R1.8-domestic-VFP
/opt/unetlab/addons/qemu/vqfxre-10K-F-20.2R1.10
The playbook
- name: Copy files to remote
hosts: test_11
tasks:
- name: cp images
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop: "{{ my_files }}"
loop_control:
label: "{{ item.src }}"
works as expected
shell> ansible-playbook pb.yml
PLAY [Copy files to remote] ******************************************************************
TASK [cp images] *****************************************************************************
changed: [test_11] => (item=images/Juniper/VMX/vmx.18.2/images/virtioa.qcow2)
changed: [test_11] => (item=images/Juniper/VMX/vmx.18.2/images/virtiob.qcow2)
changed: [test_11] => (item=images/Juniper/VMX/vmx.18.2/images/virtioc.qcow2)
changed: [test_11] => (item=images/Juniper/VMX/vmx.18.2/images/vFPC-20180605.img)
changed: [test_11] => (item=images/Juniper/VSRX/junos-vsrx3-x86-64-20.4R1.12.qcow2)
changed: [test_11] => (item=images/Juniper/QFX/vqfx-20.2R1-2019010209-pfe-qemu.qcow)
changed: [test_11] => (item=images/Juniper/QFX/vqfx-20.2R1.10-re-qemu.qcow2)
changed: [test_11] => (item=images/Cisco/L2/vios_l2-adventerprisek9-m.SSA.high_iron_20180619.qcow2)
changed: [test_11] => (item=images/Cisco/L3/vios-adventerprisek9-m.vmdk.SPA.157-3.M3)
PLAY RECAP ***********************************************************************************
test_11: ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
How to debug
Find files at the controller
- name: Stat and assert files at the controller
hosts: localhost
vars:
my_src: "{{ dict(st.results|json_query('[].[item.src, stat.exists]')) }}"
tasks:
- stat:
path: "{{ playbook_dir }}/{{ item.src }}"
register: st
loop: "{{ my_files }}"
- debug:
var: my_src
when: debug|d(false)|bool
- assert:
that: my_src_missing|length == 0
fail_msg: "Error: Missing files: {{ my_src_missing }}"
vars:
my_src_missing: "{{ my_src|dict2items|
rejectattr('value')|
map(attribute='key')|
list }}"
Create or delete directories and files for testing
- name: Create or delete directories and files
hosts: test_11
tasks:
- name: Create local
block:
- name: Create local directories
file:
state: directory
path: "{{ playbook_dir }}/{{ item }}"
loop: "{{ my_files|map(attribute='src')|map('dirname')|unique }}"
- name: Create local files
file:
state: touch
path: "{{ playbook_dir }}/{{ item.src }}"
loop: "{{ my_files }}"
delegate_to: localhost
when: create_local|d(false)|bool
- name: Create directories at remote host
file:
state: directory
path: "{{ item }}"
loop: "{{ my_files|map(attribute='dest')|map('dirname')|unique }}"
when: create_remote|d(false)|bool
- name: Delete local tree
file:
state: absent
path: "{{ playbook_dir }}/{{ item }}"
loop: "{{ my_files|map(attribute='src')|map('dirname')|unique }}"
delegate_to: localhost
when: delete_local|d(false)|bool
- name: Delete remote tree
file:
state: absent
path: "{{ item }}"
loop: "{{ my_files|map(attribute='dest')|map('dirname')|unique }}"
when: delete_remote|d(false)|bool
List of files
shell> cat group_vars/all/my_files.yml
my_files:
- {src: images/Juniper/VMX/vmx.18.2/images/virtioa.qcow2, dest: /opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP/virtioa.qcow2}
- {src: images/Juniper/VMX/vmx.18.2/images/virtiob.qcow2, dest: /opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP/virtiob.qcow2}
- {src: images/Juniper/VMX/vmx.18.2/images/virtioc.qcow2, dest: /opt/unetlab/addons/qemu/vmxvcp-18.2R1.8-domestic-VCP/virtioc.qcow2}
- {src: images/Juniper/VMX/vmx.18.2/images/vFPC-20180605.img, dest: /opt/unetlab/addons/qemu/vmxvfp-18.2R1.8-domestic-VFP/virtioa.qcow2}
- {src: images/Juniper/VSRX/junos-vsrx3-x86-64-20.4R1.12.qcow2, dest: /opt/unetlab/addons/qemu/vsrxng-20.4R1.12/virtioa.qcow2}
- {src: images/Juniper/QFX/vqfx-20.2R1-2019010209-pfe-qemu.qcow, dest: /opt/unetlab/addons/qemu/vqfxpfe-10K-F-20.2R1.10/hda.qcow}
- {src: images/Juniper/QFX/vqfx-20.2R1.10-re-qemu.qcow2, dest: /opt/unetlab/addons/qemu/vqfxre-10K-F-20.2R1.10/hda.qcow2}
- {src: images/Cisco/L2/vios_l2-adventerprisek9-m.SSA.high_iron_20180619.qcow2, dest: /opt/unetlab/addons/qemu/viosl2-adventerprisek9-m.03.2017/virtioa.qcow2}
- {src: images/Cisco/L3/vios-adventerprisek9-m.vmdk.SPA.157-3.M3, dest: /opt/unetlab/addons/qemu/vios-adventerprisek9-m.SPA.156-1.T/vios-adventerprisek9-m.SPA.156-1.T.vmdk}
Related
I have some files (file1), in some servers (group: myservers), which should look like this:
search www.mysebsite.com
nameserver 1.2.3.4
nameserver 1.2.3.5
This is an example of what this file should look like:
The first line is mandatory ("search www.mysebsite.com").
The second and the third lines are mandatory as well, but the ips can change (although they should all be like this: ...).
I've being researching to implement some tasks using Ansible to check if the files are properly configured. I don't want to change any file, only check and output if the files are not ok or not.
I know I can use ansible.builtin.lineinfile to check it, but I still haven't managed to find out how to achieve this.
Can you help please?
For example, given the inventory
shell> cat hosts
[myservers]
test_11
test_13
Create a dictionary of what you want to audit
audit:
files:
/etc/resolv.conf:
patterns:
- '^search example.com$'
- '^nameserver \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
/etc/rc.conf:
patterns:
- '^sshd_enable="YES"$'
- '^syslogd_flags="-ss"$'
Declare the directory at the controller where the files will be stored
my_dest: /tmp/ansible/myservers
fetch the files
- fetch:
src: "{{ item.key }}"
dest: "{{ my_dest }}"
loop: "{{ audit.files|dict2items }}"
Take a look at the fetched files
shell> tree /tmp/ansible/myservers
/tmp/ansible/myservers
├── test_11
│ └── etc
│ ├── rc.conf
│ └── resolv.conf
└── test_13
└── etc
├── rc.conf
└── resolv.conf
4 directories, 4 files
Audit the files. Create the dictionary host_files_results in the loop
- set_fact:
host_files_results: "{{ host_files_results|default({})|
combine(host_file_dict|from_yaml) }}"
loop: "{{ audit.files|dict2items }}"
loop_control:
label: "{{ item.key }}"
vars:
host_file_path: "{{ my_dest }}/{{ inventory_hostname }}/{{ item.key }}"
host_file_lines: "{{ lookup('file', host_file_path).splitlines() }}"
host_file_result: |
[{% for pattern in item.value.patterns %}
{{ host_file_lines[loop.index0] is regex pattern }},
{% endfor %}]
host_file_dict: "{ {{ item.key }}: {{ host_file_result|from_yaml is all }} }"
gives
ok: [test_11] =>
host_files_results:
/etc/rc.conf: true
/etc/resolv.conf: true
ok: [test_13] =>
host_files_results:
/etc/rc.conf: true
/etc/resolv.conf: true
Declare the dictionary audit_files that aggregates host_files_results
audit_files: "{{ dict(ansible_play_hosts|
zip(ansible_play_hosts|
map('extract', hostvars, 'host_files_results'))) }}"
gives
audit_files:
test_11:
/etc/rc.conf: true
/etc/resolv.conf: true
test_13:
/etc/rc.conf: true
/etc/resolv.conf: true
Evaluate the audit results
- block:
- debug:
var: audit_files
- assert:
that: "{{ audit_files|json_query('*.*')|flatten is all }}"
fail_msg: "[ERR] Audit of files failed. [TODO: list failed]"
success_msg: "[OK] Audit of files passed."
run_once: true
gives
msg: '[OK] Audit of files passed.'
Example of a complete playbook for testing
- hosts: myservers
vars:
my_dest: /tmp/ansible/myservers
audit:
files:
/etc/resolv.conf:
patterns:
- '^search example.com$'
- '^nameserver \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
/etc/rc.conf:
patterns:
- '^sshd_enable="YES"$'
- '^syslogd_flags="-ss"$'
audit_files: "{{ dict(ansible_play_hosts|
zip(ansible_play_hosts|
map('extract', hostvars, 'host_files_results'))) }}"
tasks:
- fetch:
src: "{{ item.key }}"
dest: "{{ my_dest }}"
loop: "{{ audit.files|dict2items }}"
loop_control:
label: "{{ item.key }}"
- set_fact:
host_files_results: "{{ host_files_results|default({})|
combine(host_file_dict|from_yaml) }}"
loop: "{{ audit.files|dict2items }}"
loop_control:
label: "{{ item.key }}"
vars:
host_file_path: "{{ my_dest }}/{{ inventory_hostname }}/{{ item.key }}"
host_file_lines: "{{ lookup('file', host_file_path).splitlines() }}"
host_file_result: |
[{% for pattern in item.value.patterns %}
{{ host_file_lines[loop.index0] is regex pattern }},
{% endfor %}]
host_file_dict: "{ {{ item.key }}: {{ host_file_result|from_yaml is all }} }"
- debug:
var: host_files_results
- block:
- debug:
var: audit_files
- assert:
that: "{{ audit_files|json_query('*.*')|flatten is all }}"
fail_msg: "[ERR] Audit of files failed. [TODO: list failed]"
success_msg: "[OK] Audit of files passed."
run_once: true
... implement some tasks using Ansible to check if the files are properly configured. I don't want to change any file, only check and output if the files are not ok or not.
Since Ansible is mostly used as Configuration Management Tool there is no need to check (before) if a file is properly configured. Just declare the Desired State and make sure that the file is in that state. As this is approach is working with Validating: check_mode too, if interested in a Configuration Check or an Audit it could be implemented simply as follow:
resolv.conf as is it should be
# Generated by NetworkManager
search example.com
nameserver 192.0.2.1
hosts.ini
[test]
test.example.com NS_IP=192.0.2.1
resolv.conf.j2 template
# Generated by NetworkManager
search {{ DOMAIN }}
nameserver {{ NS_IP }}
A minimal example playbook for Configuration Check in order to audit the config
---
- hosts: test
become: false
gather_facts: false
vars:
# Ansible v2.9 and later
DOMAIN: "{{ inventory_hostname.split('.', 1) | last }}"
tasks:
- name: Check configuration (file)
template:
src: resolv.conf.j2
dest: resolv.conf
check_mode: true # will never change existing config
register: result
- name: Config change
debug:
msg: "{{ result.changed }}"
will result for no changes into an output of
TASK [Check configuration (file)] ******
ok: [test.example.com]
TASK [Config change] *******************
ok: [test.example.com] =>
msg: false
or for changes into
TASK [Check configuration (file)] ******
changed: [test.example.com]
TASK [Config change] *******************
ok: [test.example.com] =>
msg: true
and depending on what's in the config file.
If one is interested in an other message text and need to invert the output therefore, just use msg: "{{ not result.changed }}" as it will report an false if true and true if false.
Further Reading
Using Ansible inventory, variables in inventory, the template module (to) Template a file out to a target host and Enforcing check_mode on tasks makes it extremely simply to prevent Configuration Drift.
And as a reference for getting the search domain, Ansible: How to get hostname without domain name?.
tasks:
- name: sync folders
loop: "{{ folder_list | list }}"
ansible.posix.synchronize:
src: "/path/folder/{{ item }}"
dest: "/other_node/folders/"
archive: false
recursive: true
perms: true
checksum: true
delete: true
The list of folders folder_list is defined somewhere else.
I do not have control of this, and cannot change it. I also do not know the folder list up front, so cannot set it statically.
It may contain items that do not exist on "this" machine.
Is there a way to have the sync task skip such items? I found stat. Looks like it can be used to check for the existence of a file or folder, but I couldn't figure out how to use it, set_fact, and synchronize together within the task to accomplish this.
What I'm trying to do is something like:
Loop through folder list > if source folder exists > sync folder to destination.
PS: Please let me know if this belongs on ServerFault instead.
The testing paths on the controller is simple. For example, given the tree
shell> tree /tmp/export/
/tmp/export/
├── dir1
│ ├── a
│ └── b
└── dir2
└── c
2 directories, 3 files
The playbook below skips missing folders
shell> cat pb.yml
- hosts: test_11
vars:
folder_list:
- /tmp/export/dir1
- /tmp/export/dir2
- /tmp/export/dir3
tasks:
- debug:
msg: "synchronize {{ item }}"
loop: "{{ folder_list }}"
when: item is directory
gives
shell> ansible-playbook pb.yml
PLAY [test_11] ***********************************************************************************************
TASK [debug] *************************************************************************************************
ok: [test_11] => (item=/tmp/export/dir1) =>
msg: synchronize /tmp/export/dir1
ok: [test_11] => (item=/tmp/export/dir2) =>
msg: synchronize /tmp/export/dir2
skipping: [test_11] => (item=/tmp/export/dir3)
PLAY RECAP ***************************************************************************************************
test_11: ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You can combine the conditions. For example,
when: item is directory or item is link
, or simply test the existence only
when: item is exists
Note: The module stat examines the files at the current host. You have to delegate this task to localhost if you want to use it here. Register the results and declare the variable below
folder_exists: "{{ dict(folder_list_stat.results|
json_query('[].[item, stat.exists]')) }}"
- block:
- stat:
path: "{{ item }}"
loop: "{{ folder_list }}"
register: folder_list_stat
- debug:
var: folder_list_stat
when: debug|d(false)|bool
- debug:
var: folder_exists
when: debug|d(false)|bool
delegate_to: localhost
run_once: true
gives the dictionary
folder_exists:
/tmp/export/dir1: true
/tmp/export/dir2: true
/tmp/export/dir3: false
Then, the condition is trivial. See the debug on what other attributes are available in folder_list_stat and create other dictionaries for testing if you want to.
Example of a complete playbook for testing
- hosts: test_11
vars:
folder_list:
- /tmp/export/dir1
- /tmp/export/dir2
- /tmp/export/dir3
folder_exists: "{{ dict(folder_list_stat.results|
json_query('[].[item, stat.exists]')) }}"
tasks:
- block:
- stat:
path: "{{ item }}"
loop: "{{ folder_list }}"
register: folder_list_stat
- debug:
var: folder_list_stat
when: debug|d(false)|bool
- debug:
var: folder_exists
when: debug|d(false)|bool
delegate_to: localhost
run_once: true
- debug:
msg: "synchronize {{ item }}"
loop: "{{ folder_list }}"
when: folder_exists[item]
How to change the value of src to use the first file found or use another path/file if none is found ignoring errors, for example, I want to have a file for a specific host, but default to another file if the first one is not found or skip, I can solve this by duplication the following block with a different src each:
- name: create file unique per host:
template:
src: "{{ ansible_host }}/{{ item }}.conf.j2"
dest: "/etc/wireguard/{{ item }}.conf"
loop: "{{ files }}"
- name: create file:
template:
src: "{{ item }}.conf.j2"
dest: "/etc/wireguard/{{ item }}.conf"
loop: "{{ files }}"
In the role directory, I have inside the templates a directory per host (IP), for example:
roles/
`templates/
| default.conf.j2
`10.1.2.3/
|wg.100.conf.j2
`wg.200.conf.j2
When the task runs against 10.1.2.3 I want to use the files wg.100.conf.j2 & wg.200.conf.j2 but for other hosts use default.conf.j2 or skip if nothing is defined
Any idea how to do it in a single task?
For example, I would like to call the playbook like this:
- roles:
- role: test_role
configs:
- wg.test-1
- wg.test-2
The expected result is to have the files wg.test-1 and wg.test-2 in all hosts:
/etc/wireguard/wg.test-1.conf
/etc/wireguard/wg.test-1.conf
But also include unique files per host, for example having the following structure within the role, host 10.1.2.3 will have the files wg.100 & wg.200 and host 10.1.2.4 the file wg.300
test_role
├── tasks
│ └── main.yml
└── templates
├── 10.1.2.3
│ ├── wg.100.conf.j2
│ └── wg.200.conf.j2
├── 10.1.2.4
│ └── wg.300.conf.j2
│
├── wg.test-2.conf.j2
└── wg.test-1.conf.j2
The reason I want to do all within a task is to simplify the restart of only changed services despite if the src is different and use something like:
- name: create config
template:
src: "{{ ansible_host }}/{{ item }}.conf.j2" || {{ item }}.conf.j2
dest: "/etc/wireguard/{{ item }}.conf"
loop: "{{ configs }}"
notify: restart service
register: changes
- set_fact:
restart_service: "{{ restart_service | default([]) + [item.item] }}"
when: item.changed
loop: "{{ changes.results }}"
no_log: true
This is the current working role that I have, but would like to find a better approach (prevent duplicating blocks)
---
- name: Create wg.file
template:
src: "{{ item }}.conf.j2"
dest: "/etc/wireguard/{{ item }}.conf"
loop: "{{ vpns }}"
notify: restart vpn
register: changes
- set_fact:
restart_service: "{{ restart_service | default([]) + [item.item] }}"
when: item.changed
loop: "{{ changes.results }}"
no_log: true
- name: find per host wg.files
set_fact:
per_host_wg_files: "{{ lookup('fileglob', 'templates/{{ ansible_host }}/*.j2', wantlist=True) }}"
- name: Create per host wg.files
template:
src: "{{ ansible_host }}/{{ item | basename }}"
dest: "/etc/wireguard/{{ (item | basename).split('.')[:3] | join('.') }}"
loop: "{{ per_host_wg_files }}"
notify: restart vpn
register: changes
- name: create fact with changes
set_fact:
restart_service: "{{ restart_service | default([]) + [(item.item | basename).split('.')[:2] | join('.')] }}"
when: item.changed
loop: "{{ changes.results }}"
no_log: true
Just in case this is the restart handler:
- name: restart vpn
systemd:
daemon_reload: true
state: restarted
enabled: true
name: "wg-quick#{{ item }}"
async: 10
poll: 5
loop: "{{ restart_service }}"
when: restart_service is defined
Example 1.
Given the tree for testing
shell> pwd
/scratch/tmp7/test-151
shell> tree .
.
├── ansible.cfg
├── hosts
├── pb.yml
└── roles
└── role_A
├── tasks
│ └── main.yml
└── templates
├── 10.1.2.3
│ ├── test-1.conf.j2
│ └── test-2.conf.j2
├── 10.1.2.4
│ └── test-2.conf.j2
└── default.conf.j2
6 directories, 8 files
shell> cat ansible.cfg
[defaults]
gathering = explicit
inventory = $PWD/hosts
roles_path = $PWD/roles
remote_tmp = ~/.ansible/tmp
retry_files_enabled = false
stdout_callback = yaml
shell> cat hosts
host1 ansible_host=10.1.2.3
host2 ansible_host=10.1.2.4
host3 ansible_host=10.1.2.5
shell> cat pb.yml
- hosts: all
vars:
files: [test-1, test-2, test-3]
findme: "{{ [ansible_host]|
product(files|
product(['.conf.j2'])|
map('join'))|
map('join', '/') + ['default.conf.j2'] }}"
found_file: "{{ lookup('ansible.builtin.first_found', found_params) }}"
found_params:
files: "{{ findme }}"
paths: ['templates']
skip: true
roles:
- role_A
shell> cat roles/role_A/tasks/main.yml
- debug:
var: findme
- debug:
var: found_file
- name: Do it in this single task
debug:
msg: "{{ lookup('template', found_file) }}"
shell> cat roles/role_A/templates/10.1.2.3/test-1.conf.j2
10.1.2.3 test-1
shell> cat roles/role_A/templates/10.1.2.3/test-2.conf.j2
10.1.2.3 test-2
shell cat roles/role_A/templates/10.1.2.4/test-2.conf.j2
10.1.2.4 test-2
shell> cat roles/role_A/templates/default.conf.j2
{{ ansible_host }} default
Create a list of files. Declare the variables
files: [test-1, test-2, test-3]
findme: "{{ [ansible_host]|
product(files|
product(['.conf.j2'])|
map('join'))|
map('join', '/') + ['default.conf.j2'] }}"
gives
TASK [role_A : debug] ************************************************************************
ok: [host3] =>
findme:
- 10.1.2.5/test-1.conf.j2
- 10.1.2.5/test-2.conf.j2
- 10.1.2.5/test-3.conf.j2
- default.conf.j2
ok: [host1] =>
findme:
- 10.1.2.3/test-1.conf.j2
- 10.1.2.3/test-2.conf.j2
- 10.1.2.3/test-3.conf.j2
- default.conf.j2
ok: [host2] =>
findme:
- 10.1.2.4/test-1.conf.j2
- 10.1.2.4/test-2.conf.j2
- 10.1.2.4/test-3.conf.j2
- default.conf.j2
Find the first file available.
The plugin first_found by default doesn't look for files in templates. You must add this subdirectory to the parameter paths when you put files into templates.
The lookup below always succeeds because each list of files is terminated by default.conf.j2.
skip: true means the lookup returns an empty list [] when no files are matched.
found_file: "{{ lookup('ansible.builtin.first_found', found_params) }}"
found_params:
files: "{{ findme }}"
paths: ['templates']
skip: true
gives
TASK [role_A : debug] ************************************************************************
ok: [host2] =>
found_file: /export/scratch/tmp7/test-151/roles/role_A/templates/10.1.2.4/test-2.conf.j2
ok: [host3] =>
found_file: /export/scratch/tmp7/test-151/roles/role_A/templates/default.conf.j2
ok: [host1] =>
found_file: /export/scratch/tmp7/test-151/roles/role_A/templates/10.1.2.3/test-1.conf.j2
Use the file.
- debug:
msg: "{{ lookup('template', found_file) }}"
If you want to allow an empty variable found_file test it
- debug:
msg: "{{ lookup('template', found_file) }}"
when: found_file|length > 0
gives
TASK [role_A : debug] ************************************************************************
ok: [host1] =>
msg: |-
10.1.2.3 test-1
ok: [host2] =>
msg: |-
10.1.2.4 test-2
ok: [host3] =>
msg: |-
10.1.2.5 default
Example 2.
Q: "Given the following structure within the role, the expected result is to have the files wg.test-1 and wg.test-2 in all hosts and also include unique files per host. Host 10.1.2.3 will have the files wg.100 and wg.200, and host 10.1.2.4 the file wg.300."
test_role
├── tasks
│ └── main.yml
└── templates
├── 10.1.2.3
│ ├── wg.100.conf.j2
│ └── wg.200.conf.j2
├── 10.1.2.4
│ └── wg.300.conf.j2
│
├── wg.test-2.conf.j2
└── wg.test-1.conf.j2
A: In this case the plugin first_found is not needed. Use the plugin fileglob instead.
Given the tree for testing
shell> tree .
.
├── ansible.cfg
├── hosts
├── pb.yml
└── roles
└── role_A
├── defaults
│ └── main.yml
├── tasks
│ └── main.yml
└── templates
├── 10.1.2.3
│ ├── wg.100.conf.j2
│ └── wg.200.conf.j2
├── 10.1.2.4
│ └── wg.300.conf.j2
├── wg.test-1.conf.j2
└── wg.test-2.conf.j2
shell> cat pb.yml
- hosts: all
roles:
- role_A
shell> cat roles/role_A/defaults/main.yml
files_global_pattern: templates/*.j2
files_global: "{{ q('ansible.builtin.fileglob', files_global_pattern) }}"
files_local_pattern: "templates/{{ ansible_host }}/*.j2"
files_local: "{{ q('ansible.builtin.fileglob', files_local_pattern) }}"
shell> cat roles/role_A/tasks/main.yml
- debug:
msg: |
files_global:
{{ files_global|to_nice_yaml }}
files_local:
{{ files_local|to_nice_yaml }}
- debug:
msg: |
src: {{ item }}
dest: {{ _dest }}
content: {{ lookup('template', item) }}
loop: "{{ files_global + files_local }}"
vars:
_dest: "{{ item|basename|splitext|first }}"
shell> find roles/role_A/templates -type f | xargs cat
{{ ansible_host }} wg.test-1
{{ ansible_host }} wg.test-2
10.1.2.3 wg.200
10.1.2.3 wg.100
10.1.2.4 wg.300
Declare the lists of global and local files
shell> cat roles/role_A/defaults/main.yml
files_global_pattern: templates/*.j2
files_global: "{{ q('ansible.builtin.fileglob', files_global_pattern) }}"
files_local_pattern: "templates/{{ ansible_host }}/*.j2"
files_local: "{{ q('ansible.builtin.fileglob', files_local_pattern) }}"
gives
TASK [role_A : debug] ************************************************************************
ok: [host1] =>
msg: |-
files_global:
- /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-1.conf.j2
- /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-2.conf.j2
files_local:
- /export/scratch/tmp7/test-152/roles/role_A/templates/10.1.2.3/wg.200.conf.j2
- /export/scratch/tmp7/test-152/roles/role_A/templates/10.1.2.3/wg.100.conf.j2
[WARNING]: Unable to find 'templates/10.1.2.5' in expected paths (use -vvvvv to see paths)
ok: [host3] =>
msg: |-
files_global:
- /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-1.conf.j2
- /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-2.conf.j2
files_local:
[]
ok: [host2] =>
msg: |-
files_global:
- /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-1.conf.j2
- /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-2.conf.j2
files_local:
- /export/scratch/tmp7/test-152/roles/role_A/templates/10.1.2.4/wg.300.conf.j2
Use the lists
- debug:
msg: |
src: {{ item }}
dest: {{ _dest }}
content: {{ lookup('template', item) }}
loop: "{{ files_global + files_local }}"
vars:
_dest: "{{ item|basename|splitext|first }}"
gives
TASK [role_A : debug] ************************************************************************
[WARNING]: Unable to find 'templates/10.1.2.5' in expected paths (use -vvvvv to see paths)
ok: [host1] => (item=/export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-1.conf.j2) =>
msg: |-
src: /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-1.conf.j2
dest: wg.test-1.conf
content: 10.1.2.3 wg.test-1
ok: [host2] => (item=/export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-1.conf.j2) =>
msg: |-
src: /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-1.conf.j2
dest: wg.test-1.conf
content: 10.1.2.4 wg.test-1
ok: [host1] => (item=/export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-2.conf.j2) =>
msg: |-
src: /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-2.conf.j2
dest: wg.test-2.conf
content: 10.1.2.3 wg.test-2
ok: [host2] => (item=/export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-2.conf.j2) =>
msg: |-
src: /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-2.conf.j2
dest: wg.test-2.conf
content: 10.1.2.4 wg.test-2
ok: [host3] => (item=/export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-1.conf.j2) =>
msg: |-
src: /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-1.conf.j2
dest: wg.test-1.conf
content: 10.1.2.5 wg.test-1
ok: [host2] => (item=/export/scratch/tmp7/test-152/roles/role_A/templates/10.1.2.4/wg.300.conf.j2) =>
msg: |-
src: /export/scratch/tmp7/test-152/roles/role_A/templates/10.1.2.4/wg.300.conf.j2
dest: wg.300.conf
content: 10.1.2.4 wg.300
ok: [host1] => (item=/export/scratch/tmp7/test-152/roles/role_A/templates/10.1.2.3/wg.200.conf.j2) =>
msg: |-
src: /export/scratch/tmp7/test-152/roles/role_A/templates/10.1.2.3/wg.200.conf.j2
dest: wg.200.conf
content: 10.1.2.3 wg.200
ok: [host3] => (item=/export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-2.conf.j2) =>
msg: |-
src: /export/scratch/tmp7/test-152/roles/role_A/templates/wg.test-2.conf.j2
dest: wg.test-2.conf
content: 10.1.2.5 wg.test-2
ok: [host1] => (item=/export/scratch/tmp7/test-152/roles/role_A/templates/10.1.2.3/wg.100.conf.j2) =>
msg: |-
src: /export/scratch/tmp7/test-152/roles/role_A/templates/10.1.2.3/wg.100.conf.j2
dest: wg.100.conf
content: 10.1.2.3 wg.100
Q: "How do you prevent the error: 'ansible_host' is undefined?"
A: Use default if you want to allow undefined variable. For example,
files_local_pattern: "templates/{{ ansible_host|d('udefined') }}/*.j2"
The result will be the warning below
[WARNING]: Unable to find 'templates/udefined' in expected paths
I'm trying to pull in a file with the same name on multiple servers and I would like to just concatenate the results, but I don't think fetch module will allow me to do this. Can someone advise on another module that I could use for this task?
Current non-working code:
- hosts: '{{ target }}'
gather_facts: false
tasks:
- name: Pull in file.log contents from servers, concatenating results
fetch:
src: '/tmp/file.log'
dest: /tmp/fetched
flat: yes
fail_on_missing: no
For example, given the files
shell> ssh admin#test_11 cat /tmp/file.log
test_11
shell> ssh admin#test_12 cat /tmp/file.log
test_12
shell> ssh admin#test_13 cat /tmp/file.log
test_13
Throttle the task and time-stamp the fetched files, e.g.
- hosts: test_11,test_12,test_13
tasks:
- fetch:
src: /tmp/file.log
dest: /tmp/fetched/file-{{ time_stamp }}.log
flat: true
fail_on_missing: false
throttle: 1
vars:
time_stamp: "{{ lookup('pipe', 'date +%Y-%m-%d_%H-%M-%S') }}"
gives
shell> tree /tmp/fetched/
/tmp/fetched/
├── file-2021-03-22_21-16-54.log
├── file-2021-03-22_21-16-58.log
└── file-2021-03-22_21-17-02.log
Then assemble the content of the files, e.g.
- assemble:
src: /tmp/fetched
regexp: '^file-.*log$'
dest: /tmp/fetched/assemble-{{ time_stamp }}.log
vars:
time_stamp: "{{ lookup('pipe', 'date +%Y-%m-%d_%H-%M-%S') }}"
delegate_to: localhost
run_once: true
gives
shell> cat /tmp/fetched/assemble-2021-03-22_21-17-07.log
test_11
test_12
test_13
If you want to speed up the transfer from many hosts (e.g. ~100) increase the number of the parallel tasks (e.g. throttle: 10). Put the name of the host into the name of the file. Otherwise, the task would overwrite the files with the same timestamp, e.g.
- fetch:
src: /tmp/file.log
dest: /tmp/fetched/file-{{ inventory_hostname }}-{{ time_stamp }}.log
flat: true
fail_on_missing: false
throttle: 3
vars:
time_stamp: "{{ lookup('pipe', 'date +%Y-%m-%d_%H-%M-%S') }}"
I am trying to copy some configuration files from /tmp to /opt.
Here first I am recursively searching for the files in /tmp and /opt directories and storing it in the variable tmp_file_path and code_file_path respectively, which has an attribute files.path that I need to use in the source and destination for copy
- name: Find files in tmp
find:
paths: /tmp/
file_type: file
recurse: yes
patterns:
- file1
- file2
- file3
register: tmp_file_path
- debug:
var: tmp_file_path
- name: Find files in code
find:
paths: /opt/
file_type: file
recurse: yes
patterns:
- file1
- file2
- file3
register: code_file_path
- debug:
var: code_file_path
Here the source file paths can be /tmp/folder1/file1, /tmp/folder2/file2, /tmp/folder13/file3.
Destinations can be /opt/folderA/file1, /opt/folderB/file3, /opt/folderC/file3
As of now I have managed to write the task as below
- name: Copy files from tmp to code directory
copy:
src: "{{item.path}}"
dest: "{{item.path}}"
remote_src: yes
with_items:
- { "{{ tmp_file_path.files }}", "{{ code_file_path.files }}" }
The copy has to be done in a single command so that I do not end up hardcoding the paths for source and destination. Can anyone help me with achieving this?
Below piece of code worked for recursively copying files from code_file_path to /tmp
- name: Copy files from code directory to tmp
copy:
src: "{{item.path}}"
dest: /tmp/
remote_src: yes
with_items: "{{code_file_path.files}}"
Try this
- name: Copy files from tmp to code directory
copy:
src: "{{ item.0 }}"
dest: "{{ item.1 }}"
remote_src: yes
with_together:
- "{{ tmp_file_path.files|map(attribute='path')|list|sort }}"
- "{{ code_file_path.files|map(attribute='path')|list|sort }}"
Try with debug first
- debug:
msg:
- "src: {{ item.0 }}"
- "dest: {{ item.1 }}"
with_together:
- "{{ tmp_file_path.files|map(attribute='path')|list|sort }}"
- "{{ code_file_path.files|map(attribute='path')|list|sort }}"
It might be useful to test the sanity first
- debug:
msg: The numbers of files do not match
when: tmp_file_path.files|map(attribute='path')|list|length !=
code_file_path.files|map(attribute='path')|list|length
Q: "This did not work as expected because the files which I am searching in code_file_path has multiple sub-directories. It is sorting on the entire file path returned by code_file_path.files rather than just the file name."
A: Create a list with both paths and names. Then sort the list by the name. For example
- set_fact:
code_files: "{{ code_files|default([]) +
[{'path': item, 'name': item|basename}] }}"
loop: "{{ code_file_path.files|map(attribute='path')|list }}"
- debug:
msg:
- "src: {{ item.0 }}"
- "dest: {{ item.1.path }}"
with_together:
- "{{ tmp_file_path.files|map(attribute='path')|list|sort }}"
- "{{ code_files|sort(attribute='name') }}"
Example
shell> tree tmp
tmp
├── file1
├── file2
└── file3
shell> tree opt
opt
├── bar
│ └── file2
├── baz
│ └── file3
└── foo
└── file1
The tasks below
- set_fact:
code_files: "{{ code_files|default([]) +
[{'path': item, 'name': item|basename}] }}"
loop: "{{ code_file_path.files|map(attribute='path')|list }}"
- debug:
var: code_files|sort(attribute='name')
- debug:
msg:
- "src: {{ item.0 }}"
- "dest: {{ item.1.path }}"
with_together:
- "{{ tmp_file_path.files|map(attribute='path')|list|sort }}"
- "{{ code_files|sort(attribute='name') }}"
give
"code_files|sort(attribute='name')": [
{
"name": "file1",
"path": "/export/test/opt/foo/file1"
},
{
"name": "file2",
"path": "/export/test/opt/bar/file2"
},
{
"name": "file3",
"path": "/export/test/opt/baz/file3"
}
]
"msg": [
"src: /export/test/tmp/file1",
"dest: /export/test/opt/foo/file1"
]
"msg": [
"src: /export/test/tmp/file2",
"dest: /export/test/opt/bar/file2"
]
"msg": [
"src: /export/test/tmp/file3",
"dest: /export/test/opt/baz/file3"
]
You may achieve this with this condition : every filenames are unique in the way that if 2 files have the same name in your list from /opt directory, then their content should be the same.
If it's the case, then you may use the with_nested loop and using a conditionnal when on filename.
For example:
- name: Copy files from tmp to code directory
copy:
src: "{{ item.0 }}"
dest: "{{ item.1 }}"
remote_src: yes
when:
- item.0|basename == item.1|basename
with_nested:
- "{{ tmp_file_path.files|map(attribute='path')|list }}"
- "{{ code_file_path.files|map(attribute='path')|list }}"
The only "problem" with this solution is that you'll run the loop a lot of times...
You may also want to use the loop synthax over with_ synthax and use some loop_control in order to choose what is printed: https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html
If you have the jinja.ext.do extension loaded (in your ansible.cfg: jinja2_extensions = jinja2.ext.do) you may then contruct a dict with your paths:
- name: Make a dict
set_fact:
files_dict: |
{%- set out_dict = dict() -%}
{%- for tmp_file in tmp_file_path.files|map(attribute='path')|list -%}
{%- do out_dict.update({tmp_file|basename: {'tmp': tmp_file}}) -%}
{%- endfor -%}
{%- for opt_file in code_file_path.files|map(attribute='path')|list -%}
{%- do out_dict[opt_file|basename].update({'opt': opt_file}) -%}
{%- endfor -%}
{{ out_dict }}
- name: Copy from dict
copy:
src: "{{ item.value.tmp }}"
dest: "{{ item.value.opt }}"
remote_src: yes
with_dict: "{{ files_dict }}"