Ansible loses variable content - ansible

I've a role with some tasks to call docker_compose. When I use a variable twice, the second task fails with error: The task includes an option with an undefined variable. The error was: 'dict object' has no attribute 'base_dir'.
It looks to me (using copy&paste) that the second call to docker_compose can't access the variable in the same scope. With other tasks (file, command, shell) it works well.
The variable is stored in roles/collabora/default/main.yml:
collabora:
base_dir: /srv/docker/collabora
and this is the inventory:
...
collabora:
hosts:
server1:
collabora_customer:
customer1:
port: 2000
customer2:
port: 2001
server2:
collabora_customer:
customer1:
port: 2000
...
This is a part of my role roles/collabora/tasks/main.yml. It iterates over all entries in host var collabora_customer.
- name: Configure Collabora instances
include_tasks: manage_collabora.yml
loop: "{{ collabora_customer | default({}) | dict2items }}"
And the manage_collabora.yml makes the work per instance.
- debug:
msg: "path: {{collabora.base_dir}}"
- name: Define location
set_fact:
instance: "{{ item }}"
collabora_path: "{{ collabora.base_dir }}/{{ item.key }}"
- name: Create "Collabora" directory
file:
path: "{{collabora_path}}"
state: directory
- name: Copy docker-compose folder "Collabora"
template:
src: "docker-compose.yml.j2"
dest: "{{collabora_path}}/docker-compose.yml"
owner: "root"
group: "root"
mode: "0640"
- name: Run docker-compose pull for "Collabora"
docker_compose:
project_src: "{{collabora_path}}"
state: present
The loop works well for the first trip per host. But the second loop failes for customer2 with error message from above in the debug message.
The funny part is, when I remove the last docker_compose task, then everything works as expected. It looks to me, as the docker_compose removes the variable from the environments - why ever.
The current result is:
PLAY [Install Collabora]
****************************************************
TASK [Gathering Facts]
****************************************************
ok: [server1]
ok: [server2]
TASK [docker_collabora : Configure Collabora instances]
****************************************************
included: roles/docker_collabora/tasks/manage_collabora.yml for server1
included: roles/docker_collabora/tasks/manage_collabora.yml for server1
included: roles/docker_collabora/tasks/manage_collabora.yml for server2
TASK [docker_collabora : debug]
****************************************************
ok: [server2] => {
"msg": "path: /srv/docker/collabora"
}
TASK [docker_collabora : Define location]
****************************************************
ok: [server2]
TASK [docker_collabora : Create "Collabora" directory]
****************************************************
ok: [server2]
TASK [docker_collabora : Copy docker-compose folder "Collabora"]
****************************************************
ok: [server2]
TASK [docker_collabora : Run docker-compose pull for "Collabora"]
****************************************************
ok: [server2]
TASK [docker_collabora : debug]
****************************************************
ok: [server1] => {
"msg": "path: /srv/docker/collabora"
}
TASK [docker_collabora : Define location]
****************************************************
ok: [server1]
TASK [docker_collabora : Create "Collabora" directory]
****************************************************
ok: [server1]
TASK [docker_collabora : Copy docker-compose folder "Collabora"]
****************************************************
ok: [server1]
TASK [docker_collabora : Run docker-compose pull for "Collabora"]
****************************************************
ok: [server1]
TASK [docker_collabora : debug]
****************************************************
fatal: [server1]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'dict object' has no attribute 'base_dir'\n\nThe error appears to be in 'roles/docker_collabora/tasks/manage_collabora.yml': line 2, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n---\n- debug:\n ^ here\n"}
PLAY RECAP
****************************************************
server1 : ok=9 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
server2 : ok=7 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
I would expect, that this would work.

Related

Ansible. Updating the hostvars variable

If, during the execution of the playbook, we change the host file in host_vars (i.e. add a new variable), how then can we get this variable in hostvars in the current execution of the playbook? When you run it again, it appears in hostvars.
UPDATE 01:
Here's an example, it doesn't work (
The task Debug 3 should display test_1 instead of VARIABLE IS NOT DEFINED!
- name: Test
hosts: mon
tasks:
- name: Debug 1
debug:
var: hostvars.mon.test_1
- name: Add vars for host_vars
delegate_to: 127.0.0.1
blockinfile:
path: "{{ inventory_dir }}/host_vars/{{ inventory_hostname }}.yml"
marker: "# {mark}: {{ item.key }}"
block: |
{{ item.key }}: {{ item.value }}
with_dict:
- {test_1: "test_1"}
- name: Debug 2
debug:
var: hostvars.mon.test_1
- name: Clear facts
meta: clear_facts
- name: Refresh inventory
meta: refresh_inventory
- name: Setup
setup:
- name: Debug 3
debug:
var: hostvars.mon.test_1
Result:
PLAY [Test] ********************************************************************
TASK [Gathering Facts] *********************************************************
ok: [mon]
TASK [Debug 1] *****************************************************************
ok: [mon] => {
"hostvars.mon.test_1": "VARIABLE IS NOT DEFINED!"
}
TASK [Add vars for host_vars] **************************************************
changed: [mon -> 127.0.0.1] => (item={'key': 'test_1', 'value': 'test_1'})
TASK [Debug 2] *****************************************************************
ok: [mon] => {
"hostvars.mon.test_1": "VARIABLE IS NOT DEFINED!"
}
TASK [Setup] *******************************************************************
ok: [mon]
TASK [Debug 3] *****************************************************************
ok: [mon] => {
"hostvars.mon.test_1": "VARIABLE IS NOT DEFINED!"
}
PLAY RECAP *********************************************************************
mon : ok=6 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
On restart:
PLAY [Test] ********************************************************************
TASK [Gathering Facts] *********************************************************
ok: [mon]
TASK [Debug 1] *****************************************************************
ok: [mon] => {
"hostvars.mon.test_1": "test_1"
}
TASK [Add vars for host_vars] **************************************************
ok: [mon -> 127.0.0.1] => (item={'key': 'test_1', 'value': 'test_1'})
TASK [Debug 2] *****************************************************************
ok: [mon] => {
"hostvars.mon.test_1": "test_1"
}
TASK [Setup] *******************************************************************
ok: [mon]
TASK [Debug 3] *****************************************************************
ok: [mon] => {
"hostvars.mon.test_1": "test_1"
}
PLAY RECAP *********************************************************************
mon : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Maybe there is a way to change hostvars manually in the process?
You can ask Ansible to reread inventory (including host_vars). Generally I'd say that changing inventory on-fly is a code smell, but there are few valid cases.
- name: Refreshing inventory, SO copypaste
meta: refresh_inventory

Using ansible variable inside gathered fact list

I'm stuck to get data from gathered fact, using calculated data as part of query.
I am using 2.9 ansible and here is my task
---
- hosts: ios
connection: network_cli
gather_facts: true
tasks:
- name: CEF OUTPUT
ios_command:
commands: sh ip cef 0.0.0.0 0.0.0.0 | i nexthop
register: cef
- set_fact:
reg_result: "{{ cef.stdout |string| regex_search('Tunnel[0-9]+')}}"
- name: IT WORKS!
debug:
msg: "{{ reg_result }}"
- name: MANUAL LIST
debug:
var: ansible_facts.net_interfaces.Tunnel3.description
- name: AUTO LIST
debug:
var: ansible_facts.net_interfaces.[reg_result].description
and here is output
PLAY [ios] **********************************************
TASK [Gathering Facts] **********************************
ok: [10.77.3.1]
TASK [CEF OUTPUT] ***************************************
ok: [10.77.3.1]
TASK [set_fact] *****************************************
ok: [10.77.3.1]
TASK [IT WORKS!] ****************************************
ok: [10.77.3.1] => {
"msg": "Tunnel3"
}
TASK [MANUAL LIST] **************************************
ok: [10.77.3.1] => {
"ansible_facts.net_interfaces.Tunnel3.description": "DMVPN via MTS"
}
TASK [AUTO LIST] ****************************************
fatal: [10.77.3.1]: FAILED! => {"msg": "template error while templating string: expected name or number. String: {{ansible_facts.net_interfaces.[reg_result].description}}"}
to retry, use: --limit #/home/user/ansible/retry/ios_find_gw_int.retry
PLAY RECAP **********************************************
10.77.3.1 : ok=5 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
You see. Now I know that my default gateway is pointing to "Tunnel3", and it is possible to get some data placing this "Tunnel3" in {{ ansible_facts.net_interfaces.Tunnel3.description }} but how to get this automatically? And I feel such nested variable in the list is a very handy tool.
Remove the dot if you use the indirect addressing
- name: AUTO LIST
debug:
var: ansible_facts.net_interfaces[reg_result].description
See Referencing key:value dictionary variables.

How to trigger handlers (notify) when using import_tasks?

When using notify on a task using import_tasks the handlers are not fired. I'm wondering why. tags are working like expected.
How to trigger handlers on imported tasks?
Example:
playbook test.yml:
- hosts: all
gather_facts: no
handlers:
- name: restart service
debug:
msg: restart service
tasks:
- import_tasks: test_imports.yml
notify: restart service
tags: test
test_imports.yml
- name: test
debug:
msg: test
changed_when: yes
- name: test2
debug:
msg: test2
changed_when: yes
Expected:
> ansible-playbook -i localhost, test.yml
PLAY [all] *************************************************************************************************************
TASK [test] ************************************************************************************************************
changed: [localhost] => {
"msg": "test"
}
TASK [test2] ***********************************************************************************************************
changed: [localhost] => {
"msg": "test2"
}
RUNNING HANDLER [restart service] **************************************************************************************
ok: [localhost] => {
"msg": "restart service"
}
...
Actual:
> ansible-playbook -i localhost, test.yml
PLAY [all] *************************************************************************************************************
TASK [test] ************************************************************************************************************
changed: [localhost] => {
"msg": "test"
}
TASK [test2] ***********************************************************************************************************
changed: [localhost] => {
"msg": "test2"
}
...
This question has been partially answered in Ansible's bug tracker here:
import_tasks is processed at parse time and is effectively replaced by the tasks it imports. So when using import_tasks in handlers you would need to notify the task names within.
Source: mkrizek's comment: https://github.com/ansible/ansible/issues/59706#issuecomment-515879321
This has been also further explained here:
imports do not work like tasks, and they do not have an association between the import_tasks task and the tasks within the imported file really. import_tasks is a pre-processing trigger, that is handled during playbook parsing time. When the parser encounters it, the tasks within are retrieved, and inserted where the import_tasks task was located. There is a proposal to "taskify" includes at ansible/proposals#136 but I don't see that being implemented any time soon.
Source: sivel comment: https://github.com/ansible/ansible/issues/64935#issuecomment-573062042
And for the good part of it, it seems it has been recently fixed: https://github.com/ansible/ansible/pull/73572
But, for now, what would work would be:
- hosts: all
gather_facts: no
handlers:
- name: restart service
debug:
msg: restart service
tasks:
- import_tasks: test_imports.yml
tags: test
And a test_imports.yml file like:
- name: test
debug:
msg: test
changed_when: yes
notify: restart service
- name: test2
debug:
msg: test2
changed_when: yes
notify: restart service
This all yields:
PLAY [all] *******************************************************************************************************
TASK [test] ******************************************************************************************************
changed: [localhost] =>
msg: test
TASK [test2] *****************************************************************************************************
changed: [localhost] =>
msg: test2
RUNNING HANDLER [restart service] ********************************************************************************
ok: [localhost] =>
msg: restart service
PLAY RECAP *******************************************************************************************************
localhost : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Then if you want to import those tasks somewhere this handler is not defined you could use the environment variable ERROR_ON_MISSING_HANDLER that helps you transform the error thrown when there is a missing handler to a "simple" warning.
e.g.
$ ANSIBLE_ERROR_ON_MISSING_HANDLER=false ansible-playbook play.yml -i inventory.yml
PLAY [all] *******************************************************************************************************
TASK [test] ******************************************************************************************************
[WARNING]: The requested handler 'restart service' was not found in either the main handlers list nor in the
listening handlers list
changed: [localhost] =>
msg: test
TASK [test2] *****************************************************************************************************
changed: [localhost] =>
msg: test2
PLAY RECAP *******************************************************************************************************
localhost : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

How to skip a template copy in Ansible if the file has a pattern?

Trying to only copy an Nginx config file if the destination file does not have a string in it.
I thought this would work:
- name: Copy nginx config file
template:
src: templates/nginx.conf
dest: /etc/nginx/sites-enabled/default
validate: grep -l 'managed by Certbot' %s
But this task fails if "managed by Certbot" isn't in the file and stops the playbook run.
How can I just skip the template copy if the destination file already has that pattern? Maybe there's a better way to get the same result?
Inspired from this other answer
You can check for the presence of a content in a file using the lineinfile module in check mode. Then you can use the result as a condition to your template task. The default in the condition is to cope with the case when the file does not exists and the found attribute is not in the registered result.
---
- name: Check for presence of "managed by Certbot" in file
lineinfile:
path: /etc/nginx/sites-enabled/default
regexp: ".*# managed by Certbot.*"
state: absent
check_mode: yes
changed_when: false
register: certbot_managed
- name: Copy nginx config file when not certbot managed
template:
src: templates/nginx.conf
dest: /etc/nginx/sites-enabled/default
when: certbot_managed.found | default(0) == 0
You could use the failed_when condition and base it on the fail message that validate generate — failed to validate — to act upon:
- name: Copy nginx config file
template:
src: templates/nginx.conf
dest: /etc/nginx/sites-enabled/default
validate: grep -l 'managed by Certbot' %s
failed_when:
- copy_config_file.failed
- copy_config_file.msg != 'failed to validate'
register: copy_config_file
Note: in when and *_when, having a list of conditions is like doing list.0 and list.1 and ...
Given the playbook:
- hosts: all
gather_facts: no
tasks:
- copy:
dest: templates/nginx.conf
content: "{{ content | default('some random content') }}"
- copy:
dest: /etc/nginx/sites-enabled/default
content: "blank"
- template:
src: templates/nginx.conf
dest: /etc/nginx/sites-enabled/default
validate: grep -l 'managed by Certbot' %s
failed_when:
- copy_config_file.failed
- copy_config_file.msg != 'failed to validate'
register: copy_config_file
- shell: cat templates/nginx.conf
register: template_content
failed_when: false
- shell: cat /etc/nginx/sites-enabled/default
register: file_content
failed_when: false
- debug:
var: template_content.stdout
- debug:
var: file_content.stdout
When run via:
ansible-playbook play.yml
It gives:
PLAY [all] *******************************************************************************************************
TASK [copy] ******************************************************************************************************
changed: [localhost]
TASK [copy] ******************************************************************************************************
changed: [localhost]
TASK [template] **************************************************************************************************
ok: [localhost]
TASK [shell] *****************************************************************************************************
changed: [localhost]
TASK [shell] *****************************************************************************************************
changed: [localhost]
TASK [debug] *****************************************************************************************************
ok: [localhost] => {
"template_content.stdout": "some random content"
}
TASK [debug] *****************************************************************************************************
ok: [localhost] => {
"file_content.stdout": "blank"
}
PLAY RECAP *******************************************************************************************************
localhost : ok=7 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now, when run with
ansible-playbook play.yml -e "content='managed by Certbot\nsome other content'"
With an extra parameter to modify the content of the template, it gives:
PLAY [all] *******************************************************************************************************
TASK [copy] ******************************************************************************************************
ok: [localhost]
TASK [copy] ******************************************************************************************************
changed: [localhost]
TASK [template] **************************************************************************************************
changed: [localhost]
TASK [shell] *****************************************************************************************************
changed: [localhost]
TASK [shell] *****************************************************************************************************
changed: [localhost]
TASK [debug] *****************************************************************************************************
ok: [localhost] => {
"template_content.stdout": "managed by Certbot\nsome other content"
}
TASK [debug] *****************************************************************************************************
ok: [localhost] => {
"file_content.stdout": "managed by Certbot\nsome other content"
}
PLAY RECAP *******************************************************************************************************
localhost : ok=7 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ansible: move on to the next task if the task is completed on one host

In ansible what i require is to check for a file is available on two hosts. But if the file is available on even one host i need to cancel the task on other host and move onto the next task. The reason why i require this is because a the next task can only be done if that particular file is available and that file can be randomly written to any of the hosts.
The following play does exactly what you want:
---
- hosts:
- server1
- server2
gather_facts: False
vars:
file_name: 'foo.bar'
tasks:
- name: wait for file
wait_for:
path: '{{ file_name }}'
state: present
timeout: 30
ignore_errors: True
- name: stat
stat:
path: '{{ file_name }}'
register: result
- name: next
debug:
msg: "File {{ file_name }} available on {{ ansible_host }}"
when: result.stat.isreg is defined and result.stat.isreg
The output is:
PLAY [server1,server2] *********************************************************
TASK [wait for file] ***********************************************************
ok: [server1]
fatal: [server2]: FAILED! => {"changed": false, "elapsed": 3, "msg": "Timeout when waiting for file foo.bar"}
...ignoring
TASK [stat] ********************************************************************
ok: [server1]
ok: [server2]
TASK [next] ********************************************************************
skipping: [server2]
ok: [server1] => {
"msg": "File foo.bar available on server1"
}
PLAY RECAP *********************************************************************
server1 : ok=3 changed=0 unreachable=0 failed=0
server2 : ok=0 changed=0 unreachable=0 failed=0
You can use the stat module to check the status like below and for also you can add the serial:1 below hosts: in your playbook
stat:
path: /path/to/something
register: p
debug:
msg: "Path exists and is a directory"
when: p.stat.isdir is defined and p.stat.isdir
https://docs.ansible.com/ansible/latest/modules/stat_module.html for more details

Resources