Looping through a product of two arrays - ansible

I have this vars file in Ansible:
for_create:
client: ["VK","SB"]
folders: ["toula","tina"]
for_delete:
client: ["VK","SB"]
folders: ["invoices","scripts"]
for_rename:
client: ["VK", "SB"]
old_name: ["home"]
new_name: ["town"]
Is it possible to do something like that in YAML? The following code in Ruby
clients = ["VK", "SB"]
folders = ["toula","tina"]
clients.each do |client|
folders.each do |folder|
puts "folder #{folder} for client #{client} created"
end
end
To sum up I want each client to create the folders ["toula", "tina"]
I have tried a lot but I can't manage to make it loop twice for each folder.
The output of the above code
folder toula for client VK created
folder tina for client VK created
folder toula for client SB created
folder tina for client SB created

Iterate the lists with_nested. For example,
- debug:
msg: "folder {{ item.1 }} for client {{ item.0 }} created"
with_nested:
- "{{ for_create.client }}"
- "{{ for_create.folders }}"
vars:
for_create:
client: [VK, SB]
folders: [toula, tina]
gives (abridged)
msg: folder toula for client VK created
msg: folder tina for client VK created
msg: folder toula for client SB created
msg: folder tina for client SB created
To rename folders zip the lists
- debug:
msg: "folder {{ item.1 }} renamed to {{ item.2 }} for client {{ item.0 }}"
with_nested:
- "{{ for_rename.client }}"
- "{{ for_rename.old_name|zip(for_rename.new_name) }}"
vars:
for_rename:
client: [VK, SB]
old_name: [home]
new_name: [town]
gives (abridged)
msg: folder home renamed to town for client VK
msg: folder home renamed to town for client SB
Q: "Is it possible to check if the folder exists inside the loop?"
A: Yes. It is. Use the parameters creates or removes. For example, given the tree
shell> tree /tmp/home/
/tmp/home/
├── SB
│   └── home
└── VK
└── home
The playbook below
shell: cat pb.yml
- hosts: localhost
tasks:
- command:
cmd: "mv {{ main_path }}/{{ item.1 }} {{ main_path }}/{{ item.2 }}"
removes: "{{ main_path }}/{{ item.1 }}"
with_nested:
- "{{ for_rename.client }}"
- "{{ for_rename.old_name|zip(for_rename.new_name) }}"
vars:
main_path: "/tmp/home/{{ item.0 }}"
for_rename:
client: [VK, SB]
old_name: [home]
new_name: [town]
moves the files only if exist
shell> ansible-playbook pb.yml
PLAY [localhost] *****************************************************************************
TASK [command] *******************************************************************************
changed: [localhost] => (item=['VK', 'home', 'town'])
changed: [localhost] => (item=['SB', 'home', 'town'])
PLAY RECAP ***********************************************************************************
localhost: ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
shell> tree /tmp/home/
/tmp/home/
├── SB
│   └── town
└── VK
└── town
2 directories, 2 files
The playbook is idempotent. The commands will not execute if the files are missing
shell> ansible-playbook pb.yml
PLAY [localhost] *****************************************************************************
TASK [command] *******************************************************************************
ok: [localhost] => (item=['VK', 'home', 'town'])
ok: [localhost] => (item=['SB', 'home', 'town'])
PLAY RECAP ***********************************************************************************
localhost: ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You can use the module command the same way also to create or remove files. The next option is using the module file. For example, the playbook below
shell> cat pb.yml
- hosts: localhost
tasks:
- file:
state: touch
path: "{{ main_path }}/{{ item.1 }}"
with_nested:
- "{{ for_create.client }}"
- "{{ for_create.folders }}"
vars:
main_path: "/tmp/home/{{ item.0 }}"
for_create:
client: [VK, SB]
folders: [toula, tina]
creates the files
shell> ansible-playbook pb.yml
PLAY [localhost] *****************************************************************************
TASK [file] **********************************************************************************
changed: [localhost] => (item=['VK', 'toula'])
changed: [localhost] => (item=['VK', 'tina'])
changed: [localhost] => (item=['SB', 'toula'])
changed: [localhost] => (item=['SB', 'tina'])
PLAY RECAP ***********************************************************************************
localhost: ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
shell> tree /tmp/home/
/tmp/home/
├── SB
│   ├── tina
│   ├── toula
│   └── town
└── VK
├── tina
├── toula
└── town
This task is not idempotent because the files will receive updated file access and modification times (similar to the way touch works from the command line). Preserve access_time and modification_time to make the task idempotent
- file:
state: touch
path: "{{ main_path }}/{{ item.1 }}"
access_time: preserve
modification_time: preserve
...

Related

How to copy first file to first host, second file to second host and so on via ansible? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 13 days ago.
Improve this question
I have 10 hosts and 10 files. I want to copy one of 10 files to corresponding host:
1_file.txt -> 1_host
2_file.txt -> 2_host
...
10_file.txt -> 10_host
I've tried to do it with zip filter, like
- name: Copy splitted files to instances
copy:
src: "{{ item.0 }}"
dest: "{{ remote_ips_file }}"
delegate_to: "{{ item.1 }}"
loop: "{{ splitted_files.files | zip(instances) | list }}"
but got an error:
{"msg": "Unexpected failure during module execution.", "stdout": ""}
Another way (from ChatGPT) was:
- name: Copy files to hosts
hosts: "{{ hosts }}"
tasks:
- name: Copy file to host
copy:
src: "{{ item.0 }}"
dest: "/tmp/{{ item.1 }}"
loop: "{{ zip(files, hosts) }}"
vars:
files: ['/path/to/file1', '/path/to/file2']
hosts: ['host1', 'host2']
But also got an error:
zip is undefined
How to do it with proper way?
Edit
I've found workaround, but it works not in parallel:
- name: Copy splitted files to instances
copy:
src: "{{ item.0.path }}"
dest: "{{ remote_ips_file }}"
delegate_to: "{{ item.1 }}"
with_together:
- "{{ splitted_files.files }}"
- "{{groups['all']}}"
Given the lists
my_files: [file1, file2]
my_hosts: [host1, host2]
Create the dictionary
hosts_files: "{{ dict(my_hosts|zip(my_files)) }}"
gives
hosts_files:
host1: file1
host2: file2
Use the dictionary in the task
- copy:
src: "{{ hosts_files[inventory_hostname] }}"
dest: /tmp
Example of a complete project for testing
shell> tree .
.
├── ansible.cfg
├── files
│   ├── file1
│   └── file2
├── hosts
└── pb.yml
1 directory, 5 files
shell> cat ansible.cfg
[defaults]
gathering = explicit
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
remote_tmp = ~/.ansible/tmp
retry_files_enabled = false
stdout_callback = yaml
shell> cat hosts
[test]
host1 ansible_host=10.1.0.61
host2 ansible_host=10.1.0.63
[test:vars]
ansible_connection=ssh
ansible_user=admin
ansible_become=yes
ansible_become_user=root
ansible_become_method=sudo
ansible_python_interpreter=/usr/local/bin/python3.8
ansible_perl_interpreter=/usr/local/bin/perl
shell> cat pb.yml
- hosts: host1,host2
vars:
my_files: [file1, file2]
my_hosts: [host1, host2]
hosts_files: "{{ dict(my_hosts|zip(my_files)) }}"
tasks:
- debug:
var: hosts_files
run_once: true
- copy:
src: "{{ hosts_files[inventory_hostname] }}"
dest: /tmp
shell> ansible-playbook pb.yml
PLAY [host1,host2] ***************************************************************************
TASK [debug] *********************************************************************************
ok: [host1] =>
hosts_files:
host1: file1
host2: file2
TASK [copy] **********************************************************************************
changed: [host1]
changed: [host2]
PLAY RECAP ***********************************************************************************
host1: ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
host2: ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
shell> ssh admin#10.1.0.61 ls -1 /tmp/file1
/tmp/file1
shell> ssh admin#10.1.0.63 ls -1 /tmp/file2
/tmp/file2

Can I use ansible-playbook "--extra-vars" to execute roles conditionally

I have an Ansible playbook I am working on and I am trying to make the execution a little more dynamic. I have two roles that don't always need to be run in conjunction, sometimes only one of them needs to be run. I have been digging into the Ansible docs, and I am wondering if I can pass --extra-vars parameters to only run a specific role.
Currently, my playbook looks like this:
---
- hosts: default
become: true
roles:
- role: upgrades
when: {{ upgrades_role }}
- role: custom-packages
when: {{ custom_packages_role }}
So, the goal is to be able to run:
ansible-playbook playbook.yml -e "upgrades_role=upgrades"
And this would only run the upgrades role and skip the custom_packages role.
Similarly, if I want to run both roles on the same hosts/system:
ansible-playbook playbook.yml -e "upgrades_role=upgrades custom_packages_role=custom-packages"
This would run both roles.
Based on my understating of Ansible syntax and the --extra-vars, -e parameter, this seems like it should work. I just want to be sure I am doing this the proper way and avoiding anti-patterns.
Ansible Version: 2.14
Let me provide you with a framework to automate the use case that you described:
Pass --extra-vars parameters to only run a specific role
Create the project
shell> ls -1
ansible.cfg
hosts
playbook.yml.j2
roles
setup.yml
shell> cat ansible.cfg
[defaults]
gathering = explicit
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
retry_files_enabled = false
stdout_callback = yaml
shell> cat hosts
localhost
The playbook setup.yml
Gets the list of the roles. Fit the variable my_roles_dir to your needs.
Creates file my_roles_order.yml with the list my_roles_order. The purpose of this file is to create the order of the roles.
Creates file my_roles_enable.yml with the dictionary my_roles_enable. The purpose of this file is to create defaults.
Creates file playbook.yml from the template. Fit the template to your needs.
shell> cat setup.yml
- hosts: localhost
vars:
my_roles_dir: "{{ lookup('config', 'DEFAULT_ROLES_PATH') }}"
tasks:
- set_fact:
my_roles: "{{ my_roles|default([]) +
lookup('pipe', 'ls -1 ' ~ item).splitlines() }}"
loop: "{{ my_roles_dir }}"
- copy:
dest: "{{ playbook_dir }}/my_roles_order.yml"
content: |
my_roles_order:
{{ my_roles|to_nice_yaml|indent(2, true) }}
force: "{{ my_roles_order_force|default(false) }}"
- include_vars: my_roles_order.yml
- copy:
dest: "{{ playbook_dir }}/my_roles_enable.yml"
content: |
my_roles_enable:
{% for role in my_roles %}
{{ role }}: false
{% endfor %}
force: "{{ my_roles_enable_force|default(false) }}"
- include_vars: my_roles_enable.yml
- template:
src: playbook.yml.j2
dest: "{{ playbook_dir }}/playbook.yml"
shell> cat playbook.yml.j2
- hosts: localhost
become: true
vars_files:
- my_roles_enable.yml
roles:
{% for role in my_roles_order %}
- role: {{ role }}
when: {{ role }}_role|default(my_roles_enable.{{ role }})|bool
{% endfor %}
Test the trivial roles
shell> tree roles/
roles/
├── current_packages
│   └── tasks
│   └── main.yml
├── custom_packages
│   └── tasks
│   └── main.yml
├── stable_packages
│   └── tasks
│   └── main.yml
└── upgrades
└── tasks
└── main.yml
8 directories, 4 files
shell> cat roles/*/tasks/main.yml
- debug:
msg: Role current_packages running ...
- debug:
msg: Role custom_packages running ...
- debug:
msg: Role stable_packages running ...
- debug:
msg: Role upgrades running ...
Run the playbook setup.yml
shell> ansible-playbook setup.yml
PLAY [localhost] ****************************************************************************************
TASK [set_fact] *****************************************************************************************
ok: [localhost] => (item=/scratch/tmp7/test-204/roles)
TASK [copy] *********************************************************************************************
changed: [localhost]
TASK [include_vars] *************************************************************************************
ok: [localhost]
TASK [copy] *********************************************************************************************
changed: [localhost]
TASK [include_vars] *************************************************************************************
ok: [localhost]
TASK [template] *****************************************************************************************
changed: [localhost]
PLAY RECAP **********************************************************************************************
localhost: ok=6 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
This creates playbook.yml
shell> cat playbook.yml
- hosts: localhost
become: true
vars_files:
- my_roles_enable.yml
roles:
- role: current_packages
when: current_packages_role|default(my_roles_enable.current_packages)|bool
- role: custom_packages
when: custom_packages_role|default(my_roles_enable.custom_packages)|bool
- role: stable_packages
when: stable_packages_role|default(my_roles_enable.stable_packages)|bool
- role: upgrades
when: upgrades_role|default(my_roles_enable.upgrades)|bool
and the files
shell> cat my_roles_order.yml
my_roles_order:
- current_packages
- custom_packages
- stable_packages
- upgrades
shell> cat my_roles_enable.yml
my_roles_enable:
current_packages: false
custom_packages: false
stable_packages: false
upgrades: false
By default, the playbook runs nothing. Here you can "pass --extra-vars parameters to only run a specific role". For example, enable the role upgrades
shell> ansible-playbook playbook.yml -e upgrades_role=true
PLAY [localhost] *****************************************************************************
TASK [current_packages : debug] **************************************************************
skipping: [localhost]
TASK [custom_packages : debug] ***************************************************************
skipping: [localhost]
TASK [stable_packages : debug] ***************************************************************
skipping: [localhost]
TASK [upgrades : debug] **********************************************************************
ok: [localhost] =>
msg: Role upgrades running ...
PLAY RECAP ***********************************************************************************
localhost: ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
If you want to change the order and/or the enablement defaults of the roles edit the files my_roles_order.yml and my_roles_enable.yml. For example, put the role upgrades in the first place and enable it by default
shell> cat my_roles_order.yml
my_roles_order:
- upgrades
- current_packages
- custom_packages
- stable_packages
shell> cat my_roles_enable.yml
my_roles_enable:
current_packages: false
custom_packages: false
stable_packages: false
upgrades: true
Update the playbook
shell> ansible-playbook setup.yml
Test it
shell> ansible-playbook playbook.yml
PLAY [localhost] *****************************************************************************
TASK [upgrades : debug] **********************************************************************
ok: [localhost] =>
msg: Role upgrades running ...
TASK [current_packages : debug] **************************************************************
skipping: [localhost]
TASK [custom_packages : debug] ***************************************************************
skipping: [localhost]
TASK [stable_packages : debug] ***************************************************************
skipping: [localhost]
PLAY RECAP ***********************************************************************************
localhost: ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
It's practical to create a file if you want to enable/disable multiple roles on the command line. For example,
shell> cat myroles.yml
upgrades_role: false
stable_packages_role: true
custom_packages_role: true
Use it in the command line
shell> ansible-playbook playbook.yml -e #myroles.yml
PLAY [localhost] *****************************************************************************
TASK [upgrades : debug] **********************************************************************
skipping: [localhost]
TASK [current_packages : debug] **************************************************************
skipping: [localhost]
TASK [custom_packages : debug] ***************************************************************
ok: [localhost] =>
msg: Role custom_packages running ...
TASK [stable_packages : debug] ***************************************************************
ok: [localhost] =>
msg: Role stable_packages running ...
PLAY RECAP ***********************************************************************************
localhost: ok=2 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
Don't display skipped hosts
shell> ANSIBLE_DISPLAY_SKIPPED_HOSTS=false ansible-playbook playbook.yml -e #myroles.yml
PLAY [localhost] *****************************************************************************
TASK [custom_packages : debug] ***************************************************************
ok: [localhost] =>
msg: Role custom_packages running ...
TASK [stable_packages : debug] ***************************************************************
ok: [localhost] =>
msg: Role stable_packages running ...
PLAY RECAP ***********************************************************************************
localhost: ok=2 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
The most common way will be to use tags; updating the example in the question:
---
- hosts: default
become: true
roles:
- role: upgrades
tags: upgrades
- role: custom-packages
tags: packages
...
So, the goal is to be able to run:
# Execute all the roles
ansible-playbook playbook.yml
# Execute only the upgrade
ansible-playbook playbook.yml -t upgrades
# Execute only the setup of custom packages
ansible-playbook playbook.yml -t packages
Here is the documentation for tags.
Using variables and when is possible, but you'll need to ensure to cast it to bool.

With ansible.posix.synchronize, is there a way to a source folder that may not exist?

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 Do I Make An Ansible Playbook Skip A Role?

I am trying to skip the upgrade part of my playbook. The key part looks like this:
hosts: linux_group
name: Upgrade the third-party application
roles:
- role: "upgradeEnv"
when: ENV == inventory_hostname
vars:
logdir: "/home/appuser/external/logs"
become_user: "{{ sudoUser }}"
become_method: sudo
become: yes
tags:
- upgrade
And the key part of the role looks like this:
- name: Upgrade database
shell: "upgradeDB.sh {{ env }}"
vars:
DBURL: "{{ user }}#{{ host }}"
no_log: True
register: register_appupgrade
tags:
- upgrade
- fail:
msg: "Upgrade errors:"
when: register_appupgrade.stderr !=""
tags:
- upgrade
I run the playbook with --skip-tags=upgrade but ansible still goes into the role and runs the tasks so I end up with tags: upgrade specified on each task.
The Upgrade database gets skipped but the fail ends the run due to the when condition.
Why is the role being run from the playbook even when the tags are supposed to be skipped?
Why is the fail task not being skipped?
Given the project for testing
shell> tree .
.
├── ansible.cfg
├── hosts
├── pb.yml
└── roles
└── upgradeEnv
└── tasks
└── main.yml
3 directories, 4 files
shell> cat ansible.cfg
[defaults]
inventory = $PWD/hosts
roles_path = $PWD/roles
remote_tmp = ~/.ansible/tmp
retry_files_enabled = false
stdout_callback = yaml
shell> cat hosts
[linux_group]
test_11
test_13
ansible [core 2.14.1]
The tags keyword means the tags are applied to all tasks at the indentation level.
If you apply tags at the play level
shell> cat pb.yml
- hosts: linux_group
roles:
- role: upgradeEnv
when: ENV == inventory_hostname
tags: upgrade
everything will be skipped
shell> ansible-playbook pb.yml --skip-tags=upgrade
PLAY [linux_group] ***************************************************************************
PLAY RECAP ***********************************************************************************
If you apply tags at the role level
shell> cat pb.yml
- hosts: linux_group
roles:
- role: upgradeEnv
when: ENV == inventory_hostname
tags: upgrade
the role will be skipped
shell> ansible-playbook pb.yml --skip-tags=upgrade
PLAY [linux_group] ***************************************************************************
TASK [Gathering Facts] ***********************************************************************
ok: [test_13]
ok: [test_11]
PLAY RECAP ***********************************************************************************
test_11: ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test_13: ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=
You'll see the same result if you omit tags at role level and apply tags at each task in the role
shell> cat pb.yml
- hosts: linux_group
roles:
- role: upgradeEnv
when: ENV == inventory_hostname
shell> cat roles/upgradeEnv/tasks/main.yml
- name: Upgrade database
command: "echo {{ env }}"
register: register_appupgrade
tags: upgrade
- debug:
msg: |
register_appupgrade.stdout: {{ register_appupgrade.stdout }}
register_appupgrade.stderr: {{ register_appupgrade.stderr }}
tags: upgrade
- fail:
msg: Upgrade errors
when: register_appupgrade.stderr != ""
tags: upgrade
If you don't skip tags the play works as expected
shell> ansible-playbook pb.yml -e ENV=test_11 -e env=test
PLAY [linux_group] ***************************************************************************
TASK [Gathering Facts] ***********************************************************************
ok: [test_11]
ok: [test_13]
TASK [upgradeEnv : Upgrade database] *********************************************************
skipping: [test_13]
changed: [test_11]
TASK [upgradeEnv : debug] ********************************************************************
skipping: [test_13]
ok: [test_11] =>
msg: |-
register_appupgrade.stdout: test
register_appupgrade.stderr:
TASK [upgradeEnv : fail] *********************************************************************
skipping: [test_11]
skipping: [test_13]
PLAY RECAP ***********************************************************************************
test_11: ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
test_13: ok=1 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0

ansible load zshenv and use ENV in another task

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

Resources