Explanation of behaviour of Ansible Playbook variables defined in vars - ansible

I am fairly new to Ansible and today while writing a new playbook I came across some behaviour I can't explain. I have been through the documentation and done my best Google-Fu, but I can't seem to find the answer.
The behaviour relates to variables defined in a playbook's "var" section and their lack of expansion into proper values when using other variables or arithmetic.
Take an example playbook with "static" values in vars:
--- # Test playbook vars
- hosts: 'all'
connection: ssh
gather_facts: false
vars:
- max_size_bytes: 10240
tasks:
- name: debug!
debug:
msg: |
"max_size_bytes: {{max_size_bytes}}"
"vars: {{vars['max_size_bytes']}}"
This outputs:
# ansible-playbook -i host1, test_vars.yml
PLAY [all] *************************************************************************************************************************
TASK [debug!] **********************************************************************************************************************
ok: [host1] => {}
MSG:
"max_size_bytes: 10240"
"vars: 10240"
Which is exactly what I would expect.
However, let's say I want to calculate that 10240 number dynamically:
--- # Test playbook vars
- hosts: 'all'
connection: ssh
gather_facts: false
vars:
- max_size_bytes: "{{10 * 1024}}"
tasks:
- name: debug!
debug:
msg: |
"max_size_bytes: {{max_size_bytes}}"
"vars: {{vars['max_size_bytes']}}"
And the new result is:
# ansible-playbook -i host1, test_vars.yml
PLAY [all] *************************************************************************************************************************
TASK [debug!] **********************************************************************************************************************
ok: [host1] => {}
MSG:
"max_size_bytes: 10240"
"vars: {{10 * 1024}}"
I get a similar output if I try to use another variable within the assignment. I came across this issue when I wanted to allow a playbook user to quick change some settings, without requiring them to calculate anything. For example:
vars:
- max_size_in_megabytes: 100 #change me if required
- max_size_bytes: "{{max_size_in_megabytes * 1024 * 1024}}"
But this didn't work as I expected, as above.
In some places the variable is expanded correctly and gives the result I would expect (i.e. the calculated value). At other times, it seems the variable is not expanded and is treated as a string as per the output for vars['max_size_bytes'].
What is the reason for this behaviour? Variable expansion and calculated values seem to work elsewhere in a playbook - why not for pre-defined variables?
If this behaviour is considered normal, what is the proper way of creating global/reusable variables that may need to be calculated on the fly?
UPDATE
I realise there may be some confusion as to what my issue actually is. That's my fault.
To try and explain better, take a look at another example:
--- # Test playbook vars
- hosts: 'localhost'
connection: local
gather_facts: false
vars:
- multiplier: 10 #change me as required
- some_value: 300
- max_size_dynamic: "{{10 * 20}}"
- max_size_static: 200
tasks:
- set_fact:
is_bigger_dynamic: "{{some_value > max_size_dynamic}}"
is_bigger_static: "{{some_value > max_size_static}}"
- name: debug!
debug:
msg: |
multiplier: {{multiplier}}
some_value {{some_value}}
Is {{some_value}} bigger than {{max_size_static}}?
is_bigger_static: {{is_bigger_static}} <-- hooray
Is {{some_value}} bigger than {{max_size_dynamic}}?
is_bigger_dynamic: {{is_bigger_dynamic}} <-- woops!
When running this playbook, you can see that the value of the conditional clauses in the set_fact task differs based on how a variable is created in vars.
Output:
PLAY [localhost] **********************************************************************************************************************
TASK [set_fact] ***********************************************************************************************************************
ok: [localhost]
TASK [debug!] *************************************************************************************************************************
ok: [localhost] => {}
MSG:
multiplier: 10
some_value 300
Is 300 bigger than 200?
is_bigger_static: True <-- hooray
Is 300 bigger than 200?
is_bigger_dynamic: False <-- woops!
The conditionals are checking the same expression: 300 > 200, but if the value 200 is derived from another expression in vars the condition is wrong.
I suspect in the case of {{some_value > max_size_dynamic}} the variable isn't being expanded (much like using vars['name\] as mentioned in the comments). So it looks like the conditional ends up being {{some_value > "{{10 * 20}}"}}.
Is this expected behaviour with set_fact? Is there anything I can do to allow expressions in vars which can be further used in set_fact tasks?
This is running the latest version of Ansible on MacOS High Sierra:
# ansible --version
ansible 2.5.0
config file = /Users/xxx/.ansible.cfg
configured module search path = [u'/Users/xxx/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/local/Cellar/ansible/2.5.0/libexec/lib/python2.7/site-packages/ansible
executable location = /usr/local/bin/ansible
python version = 2.7.14 (default, Apr 9 2018, 16:44:39) [GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.1)]

Your update to the question is completely different to original post.
And the issue with update part is this: Why a string in always "greater than" a number?
Just do the type casting:
- set_fact:
is_bigger_dynamic: "{{some_value > max_size_dynamic | int }}"
is_bigger_static: "{{some_value > max_size_static }}"
In your case some_value and max_size_static are numbers, but max_size_dynamic is a string (you can't make simple integer outside of {{...}} in ansible – only strings/booleans/lists/dictionaries).

Related

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.

Use variable for default value if imported role failed

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.

Pass integer variable to task without losing the integer type

I've got a task (actually a role, but using a task here to make the example easier) that I don't own which does some operations on a variable. It assumes the variable is an integer. I need to somehow pass it a variable and have it come through as an int, and I'm not having any luck.
Here is a super simplified version of the task that I don't own:
frob.yml
- name: Validate that frob_count is <= 100
fail: msg="{{frob_count}} is greater than 100"
when: frob_count > 100
- name: Do real work
debug: msg="We frobbed {{frob_count}} times!"
My playbook is:
- name: Frob some things
hosts: localhost
vars:
things:
- parameter: 1
- parameter: 2
- parameter: 45
tasks:
- with_items: "{{things}}"
include: frob.yml
vars:
frob_count: "{{item.parameter}}"
No matter what, I get errors like "1 is greater than 100" from frob.yml. Looks like it's getting the var as a string instead of an integer.
I've tried stuff like frob_count: "{{item.parameter | int}}" with no luck. If I could change frob.yml it'd be easy, but like I said, that's out of my control. Any thoughts?
This is on Ansible 2.6.4
Solution
Upgrade to Ansible 2.7 (currently available as the stable-2.7 branch, scheduled for GA on Oct. 4th, 2018).
Add jinja2_native=True to the [defaults] section of the ansible.cfg (or set an environment variable ANSIBLE_JINJA2_NATIVE=True.
Leave your code as in the question (i.e., frob_count: "{{item.parameter}}").
The result:
TASK [Do real work] **********************************************************************************************************************
ok: [localhost] => {
"msg": "We frobbed 1 times!"
}
TASK [Validate that frob_count is <= 100] ************************************************************************************************
skipping: [localhost]
TASK [Do real work] **********************************************************************************************************************
ok: [localhost] => {
"msg": "We frobbed 2 times!"
}
TASK [Validate that frob_count is <= 100] ************************************************************************************************
skipping: [localhost]
TASK [Do real work] **********************************************************************************************************************
ok: [localhost] => {
"msg": "We frobbed 45 times!"
}
Explanation
Currently any value returned by Jinja2 template is a string, so even if you used the int filter inside (as in {{item.parameter | int}}) the output is always rendered to a string by Ansible.
Ansible 2.7 will use (with the above parameter) a feature of Jinja 2.10 called Native Python Types and retain the data type.
When you send the variable with that include task, it turns into unicode even forcing int on your playbook. So you need to use the int filter on frob.yml like this:
frob.yml
- name: Validate that frob_count is <= 100
fail: msg="{{frob_count}} is greater than 100"
when: frob_count|int > 100
I found a solution that seems to work and it doesn't require the ANSIBLE_JINJA2_NATIVE=True env variable
If instead of
initialDelaySeconds: "{{ _initialDelaySeconds }}"
you do
initialDelaySeconds: |
{{ _initialDelaySeconds }}
you can omit the " in the jinja template and the value is not converted to string!

Build variable name dynamically and access its content with Ansible

I have an ansible playbook, which I need to compare values returned from a task to a variable loaded from metadata file.
This metadata can be in any format, and I decided to go with YAML.
What I'm trying to achieve is to build a variable name from another variable + extra stuff and then lookup this value.
I've searched for answers over the web but I couldn't find any. There are also some similar questions here on SO, but they don't address exactly my issue.
Here's the code:
temp_task.yml
---
- name: Temp task
hosts: xenservers
gather_facts: no
vars_files:
- vars/xenservers_metadata.yml
tasks:
- command: ls /home # just a dummy task..
ignore_errors: yes
- set_fact: nic={{ inventory_hostname }}.network
- debug: msg={{ nic }}
- debug: msg={{ xen_perf.network }}
xenservers_metadata.yml
---
- xen:
network:
- xenbr0: "9b8be49c-....-....-...-..."
I'm trying to get the two debug messages print the same thing. One was constructed dynamically by {{ inventory_hostname }}.network while the other is explicit variable I loaded.
TASK [debug] ********************************************************************************************************************************************************
ok: [xen_perf] => {
"msg": "xen.network"
}
TASK [debug] ********************************************************************************************************************************************************
ok: [xen] => {
"msg": [
{
"xenbr0": "9b8be49c-....-....-...-..."
}
]
}
The first debug just prints the string. The second prints the actual data I need. How can I achieve the second data output by constructing the variable/attribute dynamically?
You don’t build the variable name dynamically in your example.
All variables (not facts) are stored in vars structure and you can access them this way:
- debug:
msg: "{{ vars[inventory_hostname].network }}"

Ansible playbook with_subelements

Ansible playbook with_subelements error with 3 levels.
My Config looks like
---
Firstlevel:
- fl_number: fln1
fl_data: fld1
Secondlevel:
- sl_number: sln_f1_1
sl_data: sld_f1_1
Thirdlevel:
- tl_number: tln_s1_f1_1
tl_data: tld_s1_f1_1
- tl_number: tln_s2_f1_2
tl_data: tld_s2_f1_2
The Ansible playbook is
>cat test_threelevels.yml
---
- hosts: localhost
gather_facts: no
vars_files:
- ../vars/testConfig-var.yml
tasks:
- name: DebugWorks
debug: msg="{{ item.1.Thirdlevel }}"
with_subelements:
- Firstlevel
- Secondlevel
- name: DebugDoesNotWork
debug: msg=" Sub element Thirdlevel test"
with_subelements:
- Firstlevel
- Secondlevel
- Thirdlevel
When it is executed with
ansible-playbook -v test_threelevels.yml
the task "DebugWorks" works but the task "DebugDoesNotWork" dosent.
Output
TASK: [DebugDoesNotWork] ******************************************************
fatal: [localhost] => subelements lookup expects a list of two items, first a dict or a list, and second a string
FATAL: all hosts have already failed -- aborting
PLAY RECAP ********************************************************************
Need help in understanding if this is the right way to do and why it does not work.
Open to any suggestions.
Thanks.
The error description at least vaguely says what's meant. :)
Refer to the code to see exactly the error means here. terms is the list you pass.
if not isinstance(terms, list) or not 2 <= len(terms) <= 3:
In short: You can only go 2 levels, not 3.
The documentation does say clearly:
Optionally, you can add a third element to the subelements list, that
holds a dictionary of flags.

Resources