Use variable for default value if imported role failed - ansible

I have an ansible role that reads data from a DB. That data might not exist in which case it fails. I cannot change this role. Currently I'm using this role like:
- name: Import role and read values
import_role:
name: shared_role
register: value
This works well when the data in the DB exists and the role doesn't fail. However when that data is missing it's causing more problems. So in case of an error I want to ignore that error and use a default value.
- import_role:
name: shared_role
register: value
ignore_errors: true
- set_fact:
value: "{{ value | default({{ default_var }}) }}"
where default_var is defined in group_vars. Now this doesn't work obviously and I'm kind of stuck. How could I use a variable as a default value foor another variable registered in a role that might have failed... if that makes sense.

default will by default (sorry for this redundancy but I don't know how to write it better) replace values for undefined variables only. A variable defined as None is defined.
Fortunately, there is a second parameter that can be used to replace also "empty" vars (i.e. None, or empty string). See Documentation for this
Moreover, you cannot use jinja2 expansion while inside a jinja2 expansion already. Check the jinja2 documentation to learn more.
So you should achieve your requirement with the following expression (if all you described about your data is correct):
- set_fact:
value: "{{ value | default(default_var, true) }}"
You should not have problems with variable precedence here: set_fact and register are on the same level so the latest assignment should win. But for clarity and security, I would rename that var so you are sure you never get into trouble:
- import_role:
name: shared_role
register: role_value
ignore_errors: true
- set_fact:
value: "{{ role_value | default(default_var, true) }}"

Here is my group_vars/all.yml file. I am using food that is defined here to set in my play. you can see the value 6 in output.
logs: /var/log/messages
foo: 6
and here is my play-book
---
- name: assign a variable value in default state
hosts: localhost
tasks:
- name: set fact and then print
set_fact:
new_var: "{{ doodle | default( foo ) }}"
- name: debug
debug: msg="{{ new_var }}"
and here is the output
PLAY [assign a variable value in default state] ********************************
TASK [Gathering Facts] *********************************************************
ok: [localhost]
TASK [set fact and then print] *************************************************
ok: [localhost]
TASK [debug] *******************************************************************
ok: [localhost] => {
> "msg": "6"
}
PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

This is a simple syntax error. You can not use the {{}} in a filter.
The correct syntax is
- set_fact:
value: "{{ value | default(default_var) }}"
EDIT:
If your value is "None" and you want to replace that, you can use something like this:
- set_fact:
value: "{%- if not value is defined or value == 'None' -%}{{ default_var }}{%- else -%}{{ value }}{%- endif -%}"
Take note of the use of {{}} here.

Related

Ansible: how to fetch a value from a dictionary if the dictionary MIGHT be undefined

I need to fetch a value from a dictionary, that might be either defined or undefined, but I can't find a proper way.
Let's assume we have a dictionary:
- name: Define the dictionaty
set_fact:
my_dictionary:
my_key: my_value
Then let's assume that we had to drop the dictionary at some point in some case:
- name: Drop the dictionary
set_fact:
my_dictionary:
And then I need to check if the dictionary defined and assign a variable from one of this dictionary's keys:
- name: Fetch the parameter from the dictionary or set the default value
set_fact:
my_var: >-
(my_dictionary != None) | ternary(
{{ my_dictionary.my_key }},
'my_default_value'
)
Wrong move!
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'None' has no attribute 'my_key'\n\nThe error appears to be in '***/test.yml': line 16, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - name: Fetch the parameter from the dictionary or set the default value\n ^ here\n"}
As I understood, Ansible tries to compute the values of the parameters of ternary() in any case. Well, I understand that Ansible is not a programming language, so I shouldn't expect that it will act like a programming language. :-) But, anyhow, how to fetch a value from a dictionaty if the dictionary might be set to None?
The only (ugly) way I've found is something like that:
- name: Fetch the parameter from the dictionary
set_fact:
my_var: "{{ my_dictionary.my_key }}"
when:
my_dictionary != None
- name: Set the default value
set_fact:
my_var: 'my_default_value'
when:
my_dictionary == None
Though I'd rather use some shorter form.
Would anyone be so kind as to share a hint, please?
That's what the default() filter is for.
- hosts: localhost
vars:
foo:
baz:
bar: not the default
tasks:
- debug:
msg: "{{ foo.bar | default('my_default_value') }}"
- debug:
msg: "{{ bar.bar | default('my_default_value') }}"
- debug:
msg: "{{ baz.bar | default('my_default_value') }}"
Output:
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "my_default_value"
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "my_default_value"
}
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": "not the default"
}
Hope I understood all your requirements correctly but the code seems to be way more complicated that it needs to be.
How about define dict:
-set_fact:
my_dictionary:
1: one
If dict is defined grab the value
-set_fact:
my_var: "{{ my_dictionary.1 }}"
when: my_dictionary is defined
Else set it to value of your choice:
-set_fact:
my_var: two
when: my_dictionary is not defined
Slava Ukraini!

Ansible how to extract parts of a string?

I am working on a project where I have to perform multiple queries to the system and the steps that follow it depend on the results of the query (there are 2 to 3 different ways each query can branch out).
The results from the queries are stored in a string variable using the register module. For instance, for one of the queries I have to check if there are multiple standby servers configured and the expected output can look like this:
database role = PRIMARY
host name = random_name
service name = service1
target list = server1:10201|server4:40704|server8:52125
timeout value = 120
Where the only needed part is the target list and I need to extract each server (they are separated by the "|" and I wont know how many servers will be on the target list). So essentially I need a way to scan the string until it finds the word "target list" and then extract whatever comes after the "=" and before the new-line, each one of those values could be stored in an array I guess.
I don't even know where to start, anyone know any modules or how I can go about extracting the needed parts of the strings?
Here is the sample playbook, only the last line with regex_search is relevant here. Rest is for illustration purpose. regex_search can be used for extracting the desired part from the text:
regex_search('target list\\s+=\\s+(.*)','\\1')
above regex would capture everything after = on the line containing target list and back-referenced using \1.
- name: Sample playbook
connection: local
# gather_facts: false
hosts: localhost
vars:
data: "{{ lookup('env', 'x') }}"
tasks:
- debug:
msg: "{{ data }}"
- debug:
msg: "{{ data |regex_search('target list\\s+=\\s+(.*)','\\1')}}"
Output of the above playbook:
PLAY [Sample playbook] **********************************************************************************************************************************************
TASK [Gathering Facts] **********************************************************************************************************************************************
ok: [localhost]
TASK [debug] ********************************************************************************************************************************************************
ok: [localhost] => {}
MSG:
database role = PRIMARY
host name = random_name
service name = service1
target list = server1:10201|server4:40704|server8:52125
timeout value = 120
TASK [debug] ********************************************************************************************************************************************************
ok: [localhost] => {}
MSG:
['server1:10201|server4:40704|server8:52125']
PLAY RECAP **********************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0
Given the data registered in the variable results
result:
stdout_lines:
- database role = PRIMARY
- host name = random_name
- service name = service1
- target list = server1:10201|server4:40704|server8:52125
- timeout value = 120
use the attribute stdout_lines, e.g.
- set_fact:
servers: "{{ _val.split('|') }}"
vars:
_line: "{{ result.stdout_lines|select( 'match', '^target list.*$') }}"
_arr: "{{ _line.0.split('=')|map('trim') }}"
_val: "{{ _arr.1 }}"
gives
servers:
- server1:10201
- server4:40704
- server8:52125
Use cli_parse if you have to repeatedly parse the same structures.

Generate Ansible fact once (per host)

I would like to generate a password and some other values when they do not exist yet.
Something like this:
- name: Retrieve or generate my_password
generated_fact:
shell: some shell command
name: my_password
I have a working approach but am really unhappy with it since it is very verbose and error prone:
- name: Generate my_password
shell: some shell command
register: generate_password_task
when: ansible_local.convoluted.bs.my_password is not defined
- name: Store my_password as local fact
ini_file:
path: "/etc/ansible/facts.d/convoluted.fact"
section: bs
option: my_password
value: "{{ generate_password_task.stdout }}"
when: generate_password_task.changed
- name: Reload Ansible local facts
setup: filter=ansible_local
when: generate_password_task.changed
Is there a high level abstraction for this task along the lines of the first code snippet? Or some other approach more sane than what I currently have?
The basic approach is the right one. There are few imperfections I see. One of them is 'dependence chain', when different tasks are depend on different conditions. It's hard to debug.
So, improvement one is to make a single condition:
- block:
when: ansible_local.convoluted.bs.my_password is not defined
- name: Generate my_password
shell: some shell command
register: generate_password_task
- name: Store my_password as local fact
ini_file:
path: "/etc/ansible/facts.d/convoluted.fact"
section: bs
option: my_password
value: "{{ generate_password_task.stdout }}"
- name: Reload Ansible local facts
setup: filter=ansible_local
The second trick is to use meta: end_host, it allows to terminate play for the specific host without errors and additional skips.
- hosts: ...
tasks:
- meta: end_play
when: ansible_local.convoluted.bs.my_password is defined
- name: Generate my_password
shell: some shell command
register: generate_password_task
- name: Store my_password as local fact
ini_file:
path: "/etc/ansible/facts.d/convoluted.fact"
section: bs
option: my_password
value: "{{ generate_password_task.stdout }}"
- name: Reload Ansible local facts
setup: filter=ansible_local
But you need to keep it as a separate play to use it with 'end_host'.
Here would be my approach, in two tasks.
Note that this has been checked when:
The ini file does not exists
The ini file is present but there is no bs section
The ini file is present, have a bs section, but the section does not include a my_password option
The ini file is present, have a bs section, but the option my_password is empty
The ini file is present, have a bs section and have a value for the option my_password
The cases 1 to 4 does end in a change of the ini file with a newly generated password, when the last case ends in a skipped task.
And here are the two tasks doing this:
- set_fact:
actual_password: "{{ lookup('ini', 'my_password section=bs file=/etc/ansible/facts.d/convoluted.fact', errors='ignore') }}"
- ini_file:
path: /etc/ansible/facts.d/convoluted.fact
section: bs
option: my_password
value: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,hexdigits,punctuation') }}"
when: actual_password|length == 0
Those are using
the ini lookup in order to confirm that the ini and value are not yet set
the password plugin to generate a password
The condition (when: actual_password|length == 0) to write the ini is based on experimentation
Thanks to the errors='ignore' syntax in the ini lookup, the variable actual_password ends up being an empty string if the file or section does not exists, because both those cases are causing the lookup to return an error
If the section exists but does not contains the option then the lookup ends up returning an empty array []
If the option is there but empty, the lookup return that empty string
Knowing all that is making the test relevant: []|length == 0 and ''|length == 0 are both true.
A last note on the password plugin, yes it does write a file on your controller host, but you are not forced to consider it, use it, or even, well, store it.
The plugin return the password, so you can easily use it as you would for any other variable in the value attribute of your ini_file module.
And then, if you don't want to store it on the controller host, do as you would do for anything you don't care about in linux, redirect it to /dev/null, either way, the password is in your ini file now.
And if you need to re-use that same password later in the playbook, just store it via an extra set_fact
- set_fact:
actual_password: "{{ lookup('ini', 'my_password section=bs file=/etc/ansible/facts.d/convoluted.fact', errors='ignore') }}"
- block:
- set_fact:
new_password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,hexdigits,punctuation') }}"
- ini_file:
path: /etc/ansible/facts.d/convoluted.fact
section: bs
option: my_password
value: "{{ new_password }}"
when: actual_password|length == 0
Given this playbook:
- hosts: local
gather_facts: no
tasks:
- set_fact:
actual_password: "{{ lookup('ini', 'my_password section=bs file=/etc/ansible/facts.d/convoluted.fact', errors='ignore') }}"
- ini_file:
path: /etc/ansible/facts.d/convoluted.fact
section: bs
option: my_password
value: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,hexdigits,punctuation') }}"
when: actual_password|length == 0
Here is a double run of it:
/ansible # cat /etc/ansible/facts.d/convoluted.fact
cat: can't open '/etc/ansible/facts.d/convoluted.fact': No such file or directory
/ansible # ansible-playbook play.yml
PLAY [local] ***********************************************************************************************************************************************************************************************
TASK [set_fact] ********************************************************************************************************************************************************************************************
[WARNING]: Unable to find '/etc/ansible/facts.d/convoluted.fact' in expected paths (use -vvvvv to see paths)
ok: [local]
TASK [ini_file] ********************************************************************************************************************************************************************************************
changed: [local]
PLAY RECAP *************************************************************************************************************************************************************************************************
local : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
/ansible # cat /etc/ansible/facts.d/convoluted.fact
[bs]
my_password = fnI;L3FpR5207,8,jxGP
/ansible # ansible-playbook play.yml
PLAY [local] ***********************************************************************************************************************************************************************************************
TASK [set_fact] ********************************************************************************************************************************************************************************************
ok: [local]
TASK [ini_file] ********************************************************************************************************************************************************************************************
skipping: [local]
PLAY RECAP *************************************************************************************************************************************************************************************************
local : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0

ansible: 'default' filter that treats the empty string as not defined

If I do this:
- set_fact:
NEW_VARIABLE: "{{ VARIABLE | default('default') }}"
and VARIABLE is the empty string (""), than the default not triggering.
I could do this:
- set_fact:
NEW_VARIABLE: "{{ VARIABLE | default('default') }}"
- set_fact:
NEW_VARIABLE: "default"
when: VARIABLE == ""
But I actually want to do this in a loop. So it would be much easier if I could do this using ansible filters and not conditionals.
Is this possible? Are there ansible filters that work like default but treats "" as not defined?
You have to set second argument of default to true
{{ VARIABLE | default('default', true) }}
https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#defaulting-undefined-variables
Yes, its possible.
Dont know if its something like that you want but, for your description, this will work for you...
- hosts: localhost
vars:
VARIABLE: ""
tasks:
- set_fact:
NEW_VARIABLE: '{{ (VARIABLE |length > 0) | ternary(VARIABLE, "default") }}'
- debug: msg="{{ NEW_VARIABLE }}"
PLAY [localhost] ********************************************************************************************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************************************************************************************
ok: [localhost]
TASK [set_fact] *********************************************************************************************************************************************************************************
ok: [localhost]
TASK [debug] ************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "default"
}
Im assuming that your variable will always be defined as "". By assuming that, its possible check its length and then use the ternary filter. If its bigger than 0, it use the variable value and if not, it will set NEW_VARIABLE to "default". If you dont know if VARIABLE will be defined or not, put a when: VARIABLE is defined on your set_fact task and this should be it.
Source: Ansible filters

Ansible check if variables are set

I want to automate the installation process of our software for our client. Therefore I wrote an Ansible playbook which has a task which should check if all the mandatory variables are set:
- name: Check environment variables.
hosts: all
vars_files:
- required_vars.yml
tasks:
- fail: msg="Variable '{{ item }}' is not defined"
when: item not in hostvars[inventory_hostname]
with_items:
- required_vars
The required_vars.yml looks like this:
required_vars:
- APPHOME: /home/foo/bar
- TMPDIR: /home/foo/bar/tmp
When I execute the playbook via ansible-playbook -i inventory/dev.yml playbook.yml I get the following error:
TASK [Gathering Facts] *************************************************************************************************************************************************************************************************************************ok: [localhost]
TASK [fail] ************************************************************************************************************************************************************************************************************************************failed:
[localhost] (item=required_vars) => {"changed": false, "failed": true, "item": "required_vars", "msg": "Variable 'required_vars' is not defined"}
It is obvious that I am doing something wrong, but I cannot point to the error. Can you help me please?
Edit: the accepted answer helped me out. Thank you.
But I have two more questions:
The executed task says:
TASK [fail]
skipping: [some_ip] => (item=/root)
skipping: [some_ip] => (item=TMPDIR: /home/foo/bar/tmp)
It is getting skipped because all variables are set, correct?
I think I figured out how to print the correct message, if the variable is not set:
- name: Check environment variables.
hosts: all
vars_files:
- required_vars.yml
tasks:
- fail:
msg: "Variable '{{ item }}' is not defined"
with_items: "{{ required_vars }}"
when: item is undefined
Correct? Or is there a better solution?
Two problems here:
You want to iterate over the value of required_vars variable value, so you need to provide it as an argument to with_items: "{{ required_vars }}":
with_items: "{{ required_vars }}"
Currently you are providing a list of a single element with a statically defined string required_vars.
You need to change the data type of the elements in your required_vars list to strings:
required_vars:
- "APPHOME: /home/foo/bar"
- "TMPDIR: /home/foo/bar/tmp"
Currently (because of : followed by space) you defined dictionaries, so for example in the first iteration item will have a value of { "APPHOME": "/home/foo/bar" }, which will then always fail on the when condition.
Bonus problem:
you defined a message in the form "Variable '{{ item }}' is not defined";
Ansible reports Variable 'required_vars' is not defined;
the above is not an error, as you think ("I get the following error"), but a correct result of the fail module with the message you defined yourself.
Since you have only one value for 'with_items' I think it should look like this:
with_items: "{{ required_vars }}"
On one line and with the brackets and quotation marks. Once you have more then one item, you can use the list like you did:
with_items:
- "{{ one }}"
- "{{ two }}"

Resources