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
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?.
I'd like to load a zshenv file (using source command) and then use the ENVs in another task.
This is what I have. I'm hoping there's a better solution
files
directory structure
.
├── ansible.cfg
├── hosts.yaml
├── profiles
│ └── macos.yaml
├── roles
│ └── base
│ ├── tasks
│ │ ├── git.yaml
│ │ └── main.yaml
│ └── vars
└── tools
└── zsh
└── .zshenv
./ansible.cfg
[defaults]
inventory = ./hosts.yaml
roles_path = ./roles/
stdout_callback = yaml
./hosts.yaml
---
all:
hosts:
localhost
./profiles/macos.yaml
---
# run MacOS configs
# - hosts: localhost
# connection: local
# tags: macos
# roles:
# - macos
# # when: ansible_distribution == "MacOSX"
- hosts: localhost
connection: local
tags: base
roles:
- base
./roles/base/main.yaml
---
- import_tasks: tasks/git.yaml
./roles/base/git.yaml
---
- name: source zshenv
shell:
cmd: source ../tools/zsh/.zshenv; echo $GIT_CONFIG_PATH
register: gitConfigPath
- name: Link gitconfig file
file:
# PWD: ./profiles
src: "{{ ansible_env.PWD }}/../tools/git/.gitconfig"
dest: "{{ gitConfigPath.stdout }}"
state: link
# - name: print ansible_env
# debug:
# msg: "{{ ansible_env }}"
#
# - name: print gitConfigPath
# debug:
# msg: "{{ gitConfigPath.stdout }}"
#
./tools/zsh/.zshenv
export XDG_CONFIG_HOME="$HOME/.config"
export GIT_CONFIG_PATH="$XDG_CONFIG_HOME/git/config"
command to run
ansible-playbook profiles/macos.yaml -v
PS: It'd be easier to do something like this in ansible
source tools/zsh/.zshenv && ansible-playbook profiles/macos.yaml -v
Given the simplified project without a role
shell> tree -a .
.
├── ansible.cfg
├── hosts
├── pb.yml
└── tools
├── git
└── zsh
└── .zshenv
shell> cat hosts
localhost
shell> cat tools/zsh/.zshenv
export GIT_CONFIG_PATH=/home/admin/git/.gitconfig
export ENV1=env1
export ENV2=env2
export ENV3=env3
eval "$(direnv hook zsh)"
Parse the environment on your own. For example
zshenv: "{{ dict(lookup('file', 'tools/zsh/.zshenv').splitlines()|
select('match', '^\\s*export .*$')|
map('regex_replace', '^\\s*export\\s+', '')|
map('split', '=')) }}"
gives
zshenv:
ENV1: env1
ENV2: env2
ENV3: env3
GIT_CONFIG_PATH: /home/admin/git/.gitconfig
Then, use the dictionary zshenv
- name: Link gitconfig file
file:
dest: "{{ playbook_dir }}/tools/git/.gitconfig"
src: "{{ zshenv.GIT_CONFIG_PATH }}"
state: link
gives, running with --check -- diff options
TASK [Link gitconfig file] *******************************************************************
--- before
+++ after
## -1,2 +1,2 ##
path: /export/scratch/tmp7/test-116/tools/git/.gitconfig
-state: absent
+state: link
changed: [localhost]
Notes
Example of a complete playbook for testing
shell> cat pb.yml
- hosts: localhost
vars:
zshenv: "{{ dict(lookup('file', 'tools/zsh/.zshenv').splitlines()|
select('match', '^\\s*export .*$')|
map('regex_replace', '^\\s*export\\s+', '')|
map('split', '=')) }}"
tasks:
- debug:
var: zshenv
- name: Link gitconfig file
file:
dest: "{{ playbook_dir }}/tools/git/.gitconfig"
src: "{{ zshenv.GIT_CONFIG_PATH }}"
state: link
gives
shell> ansible-playbook pb.yml
PLAY [localhost] *****************************************************************************
TASK [debug] *********************************************************************************
ok: [localhost] =>
zshenv:
ENV1: env1
ENV2: env2
ENV3: env3
GIT_CONFIG_PATH: /home/admin/git/.gitconfig
TASK [Link gitconfig file] *******************************************************************
changed: [localhost]
PLAY RECAP ***********************************************************************************
localhost: ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The link tools/git/.gitconfig -> /home/admin/git/.gitconfig was created
shell> tree -a .
.
├── ansible.cfg
├── hosts
├── pb.yml
└── tools
├── git
│ └── .gitconfig -> /home/admin/git/.gitconfig
└── zsh
└── .zshenv
You can use the dictionary zshenv to set the environment. For example,
- command: echo $GIT_CONFIG_PATH
environment: "{{ zshenv }}"
register: out
- debug:
var: out.stdout
gives
out.stdout: /home/admin/git/.gitconfig
Cache the dictionary if you want to use this environment globally in the whole play. For example,
shell> grep fact_caching ansible.cfg
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_cache
fact_caching_prefix = ansible_facts_
fact_caching_timeout = 86400
- set_fact:
zshenv: "{{ dict(lookup('file', 'tools/zsh/.zshenv').splitlines()|
select('match', '^\\s*export .*$')|
map('regex_replace', '^\\s*export\\s+', '')|
map('split', '=')) }}"
cacheable: true
Then,
- hosts: localhost
environment: "{{ zshenv }}"
tasks:
- command: echo $GIT_CONFIG_PATH
register: out
- debug:
var: out.stdout
gives
PLAY [localhost] *****************************************************************************
TASK [command] *******************************************************************************
changed: [localhost]
TASK [debug] *********************************************************************************
ok: [localhost] =>
out.stdout: /home/admin/git/.gitconfig
PLAY RECAP ***********************************************************************************
localhost: ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
With the help from Vladimir Botka, using the answer from https://stackoverflow.com/a/74924664/3053548
I modified a little bit of his code
TLDR
source the zshenv file
print out all the ENV in the shell session
store the output to as ansible facts
access the ENV in a task
Files
./profiles/macos.yaml
---
# run MacOS configs
- hosts: localhost
connection: local
tags: always
tasks:
- name: source zshenv
shell:
cmd: source ../tools/zsh/.zshenv; env
register: out
changed_when: false
- name: store zshenv as fact
set_fact:
zshenv: "{{ dict(out.stdout.splitlines() | map('split', '=')) }}"
changed_when: false
# - hosts: localhost
# connection: local
# tags: macos
# roles:
# - macos
# # when: ansible_distribution == "MacOSX"
- hosts: localhost
connection: local
tags: base
roles:
- base
./roles/base/tasks/git.yaml
---
- name: Link gitconfig file
file:
src: "{{ ansible_env.PWD }}/../tools/git/.gitconfig"
dest: "{{ zshenv.GIT_CONFIG_PATH }}"
state: link
command to run
ansible-playbook profiles/macos.yaml
I need help because i'm stuck since 2 days with a playbook.
First of all, i have a YAML file which contains jinja templates :
---
template lab:
- first_template.j2
- second_template.j2
- third_template.j2
It exists a YAML file which have value for each router, stores in "./yml/{{ inventory_hostname }}.yml"
I have a playbook Ansible which needs to use jinja templates for generate a .conf file.
---
- name: Generate .conf file
hosts: my_routers
gather_facts: no
vars:
- jinja_templates: "{{ (lookup('template', './template_list.yml') | from_yaml).template_lab }}"
vars_files:
- "./yml/{{ inventory_hostname }}.yml"
tasks:
- name: test
debug:
msg: "{{ jinja_templates }}"
- name: Generate configuration files
template:
src: "./templates/{{ jinja_templates }}"
dest: "./tmp/general/{{ inventory_hostname }}.conf"
mode: "0644"
OUTPUT of the playbook when i play it :
Could not find or access './templates/['first_template.j2', 'second_template.j2', 'third_template'.j2']'
1 : The first issue is that { jinja_templates }} output contains "[" "]" and "'" "'", so it is impossible for Ansible to use the Jinja Templates.
2 : How can i do an interation for use all jinja templates and generate configuration in a single file ?
I know that i don't use the correct structure of Ansible' Playbook ! It is for the moment experimental test :)
Thank you
Q: "Use all jinja templates and generate configuration in a single file."
A: Iterate the list of templates and create content. For example, given the tree
shell> tree .
.
├── ansible.cfg
├── hosts
├── pb.yml
├── template_list.yml
└── templates
├── first_template.j2
├── second_template.j2
└── third_template.j2
shell> cat hosts
[my_routers]
test_11
test_12
test_13
shell> cat template_list.yml
template_list:
- first_template.j2
- second_template.j2
- third_template.j2
shell> cat templates/first_template.j2
Content of the 1 template.
shell> cat templates/second_template.j2
Content of the 2 template.
shell> cat templates/third_template.j2
Content of the 3 template.
The playbook
shell> cat pb.yml
- hosts: my_routers
vars_files:
template_list.yml
tasks:
- copy:
dest: "/tmp/{{ inventory_hostname }}.conf"
content: |
{% for file in template_list %}
{{ lookup('template', file) }}
{%- endfor %}
will create the files
shell> ssh admin#test_11 cat /tmp/test_11.conf
Content of the 1 template.
Content of the 2 template.
Content of the 3 template.
shell> ssh admin#test_12 cat /tmp/test_12.conf
Content of the 1 template.
Content of the 2 template.
Content of the 3 template.
shell> ssh admin#test_13 cat /tmp/test_13.conf
Content of the 1 template.
Content of the 2 template.
Content of the 3 template.
You're passing the template files names as one line:
- debug:
msg: "{{ jinja_templates }}"
The output is being an array:
TASK [test] *******************************************************************************
ok: [localhost] => {
"msg": [
"first_template.j2",
"second_template.j2",
"third_template.j2"
]
}
You should loop over it to get them as single values:
- debug:
msg: "{{ item }}"
loop: "{{ jinja_templates }}"
TASK [test] *******************************************************************************
ok: [localhost] => (item=first_template.j2) => {
"msg": "first_template.j2"
}
ok: [localhost] => (item=second_template.j2) => {
"msg": "second_template.j2"
}
ok: [localhost] => (item=third_template.j2) => {
"msg": "third_template.j2"
}
Besides that you defined the same destination file in your template task dest: "./tmp/general/{{ inventory_hostname }}.conf".
That would lead to only writing the last template file.
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}
Use case
List all files in a directory with the format - a1.{{ env }}.js, a2.{{ env }}.js
Find corresponding files in the destination directory with the format - a1.js, a2.js
Copy a1.{{ env }}.js in the directory where a1.js exists, Copy a2.{{ env }}.js in the directory where a2.js exists
Sample code: This code does a direct find and replace
- name: Find files in archive
find:
paths: "archive/"
file_type: file
recurse: yes
register: tmp_file_path
- name: Find files in code matching names in archive
find:
paths: "code/"
file_type: file
recurse: yes
patterns: "{{ tmp_file_path.files | map(attribute='path') | map('basename') | list }}"
register: code_file_path
- set_fact:
code_files: "{{ code_files|default([]) +
[{'path': item, 'name': item|basename}] }}"
loop: "{{ code_file_path.files|map(attribute='path')|list }}"
- name: Copy files from archive to code directory
command: cp "{{ item.0 }}" "{{ item.1.path }}"
when:
- item.0|basename == item.1.path|basename
with_together:
- "{{ tmp_file_path.files|map(attribute='path')|list|sort }}"
- "{{ code_files|sort(attribute='name') }}"
Below listed is the directory structure
├── archive
│ ├── a1.test.js
│ ├── a2.test.js
│ ├── a3.test.js
│ └── a4.test.js
└── code
├── a1.js
├── dir1
│ └── a2.js
└── dir2
├── a4.js
└── dir3
└── a3.js
Copy archive/a1.test.js to code/
Copy archive/a2.test.js to code/dir1/
Copy archive/a3.test.js to code/dir1/dir2/dir3/
Copy archive/a4.test.js to code/dir1/dir2/
Is there a solution to do a direct copy as per the above use case?
Some explanation on the approach:
The entire idea of it is based on the creation of a dictionary instructing the playbook on which file should be added where.
The dictionary, for your use case would look like this:
{
"a1.js": {
"archive": "archive/a1.test.js",
"paths": [
"code"
]
},
"a2.js": {
"archive": "archive/a2.test.js",
"paths": [
"code/dir1"
]
},
"a3.js": {
"archive": "archive/a3.test.js",
"paths": [
"code/dir1/dir2/dir3"
]
},
"a4.js": {
"archive": "archive/a4.test.js",
"paths": [
"code/dir1/dir2"
]
}
}
Where the keys are the files we are searching for under the code folder, the key archive represent the file we aim to copy from the archive folder and paths is an array where the said file should find its destination(s).
Most of the logic is done by the Ansible filter regex_replace, that extract the name of the file to find in the code folder via a quite simple expression: (.*)\..*\.js$
item.path | basename | regex_replace('(.*)\\..*\\.js$', '\\1.js')
Another filter used here that might be interesting to explore is the combine filter, with the parameter recursive=true, that allows to create the paths where the files should find their destination(s).
There is also a Python operation used here: dict.keys(), in order to create the comma separated list of files to search in the code folder out of the keys of the dictionary above.
It is also making use of loop with the subelement filter, to traverse both the dictionary and its sub array paths at the same time.
And to be complete, here are the other, more commonly used filters used in this playbook:
default: to specify a default value when a variable is not defined
basename: to get only the name of a file out of its full path
dirname: to get only the directory from a full path to a file
join: to concatenate the elements of an array, here, based on a separator
Yes, it might be over-engineered compared to your use case, the playbook here-under is able to cope with the fact that a1.js could be in two different folders and will be able to copy a1.test.js in both those folders.
So, here is a solution:
- hosts: localhost
gather_facts: no
tasks:
- find:
paths: archive
file_type: file
recurse: yes
register: archives
- set_fact:
searches: "{{ searches | default({}) | combine({ key: value }) }}"
vars:
key: "{{ item.path | basename | regex_replace('(.*)\\..*\\.js$', '\\1.js') }}"
value: "{{ { 'archive': item.path, 'paths': [] } }}"
loop: "{{ archives.files }}"
loop_control:
label: "{{ item.path }}"
- find:
path: code
file_type: file
recurse: yes
pattern: "{{ searches.keys() | join(',') }}"
register: paths
- set_fact:
searches: "{{ searches | combine({key: value}, recursive=true) }}"
vars:
key: "{{ item.path | basename }}"
value: "{{ { 'paths': [item.path | dirname] + searches[item.path | basename].paths } }}"
loop: "{{ paths.files }}"
loop_control:
label: "{{ item.path }}"
- copy:
src: "{{ item.0.archive }}"
dest: "{{ item.1 ~ '/' ~ item.0.archive | basename }}"
loop: "{{ searches | subelements('paths') }}"
loop_control:
label: "{{ item.0.archive }}"
Situation before:
tree archive code
archive
├── a1.test.js
├── a2.test.js
├── a3.test.js
└── a4.test.js
code
├── a1.js
└── dir1
├── a2.js
└── dir2
├── a4.js
└── dir3
└── a3.js
3 directories, 8 files
Recap of the playbook:
PLAY [localhost] **************************************************************************************************
TASK [find] *******************************************************************************************************
ok: [localhost]
TASK [set_fact] ***************************************************************************************************
ok: [localhost] => (item=archive/a3.test.js)
ok: [localhost] => (item=archive/a2.test.js)
ok: [localhost] => (item=archive/a1.test.js)
ok: [localhost] => (item=archive/a4.test.js)
TASK [find] *******************************************************************************************************
ok: [localhost]
TASK [set_fact] ***************************************************************************************************
ok: [localhost] => (item=code/a1.js)
ok: [localhost] => (item=code/dir1/a2.js)
ok: [localhost] => (item=code/dir1/dir2/a4.js)
ok: [localhost] => (item=code/dir1/dir2/dir3/a3.js)
TASK [copy] *******************************************************************************************************
changed: [localhost] => (item=archive/a3.test.js)
changed: [localhost] => (item=archive/a2.test.js)
changed: [localhost] => (item=archive/a1.test.js)
changed: [localhost] => (item=archive/a4.test.js)
PLAY RECAP ********************************************************************************************************
localhost : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Situation after:
tree code
code
├── a1.js
├── a1.test.js
└── dir1
├── a2.js
├── a2.test.js
└── dir2
├── a4.js
├── a4.test.js
└── dir3
├── a3.js
└── a3.test.js
3 directories, 8 files