Expressions are not evaluated in the nested ansible variables - ansible

$ ls play.d/roles/debug/*
play.d/roles/debug/tasks:
main.yml
play.d/roles/debug/vars:
main.yml
$ cat play.d/roles/debug/tasks/main.yml
- debug: msg="{{ name }}"
- debug: msg="{{ vars[name]['test_var'] }}"
- debug: msg="{{ vars['nested_var']['test_var'] }}"
- debug: msg="{{ test_var }}"
$ cat play.d/roles/debug/vars/main.yml
test_var: "{{ 'value-1' if cpu == 'x64' else 'value-2' }}"
nested_var:
test_var: "{{ 'value-1' if cpu == 'x64' else 'value-2' }}"
$ cat play.d/debug.yml
- hosts: all
gather_facts: yes
roles:
- debug
$ cat inv.d/inv
[all:vars]
cpu = 'x64'
[test-srv]
52.19.xxx.xxx
With this pretty straightforward setup I expect that Ansible should evaluate inline conditionals regardless of their position (top level or nested). However it seems, that nested variables become literal strings of the expressions:
# ansible-playbook -i inv.d/inv play.d/debug.yml -e name=nested_var -l test-srv
PLAY [all] ********************************************************************
GATHERING FACTS ***************************************************************
ok: [52.19.xxx.xxx]
TASK: [debug | debug msg="{{ name }}"] ****************************************
ok: [52.19.xxx.xxx] => {
"msg": "nested_var"
}
TASK: [debug | debug msg="{{ vars[name]['test_var'] }}"] **********************
ok: [52.19.xxx.xxx] => {
"msg": "{{'value-1' if cpu == 'x64' else 'value-2'}}"
}
TASK: [debug | debug msg="{{ vars['nested_var']['test_var'] }}"] **************
ok: [52.19.xxx.xxx] => {
"msg": "{{'value-1' if cpu == 'x64' else 'value-2'}}"
}
TASK: [debug | debug msg="{{ 'value-1' if cpu == 'x64' else 'value-2' }}"] ****
ok: [52.19.xxx.xxx] => {
"msg": "value-1"
}
PLAY RECAP ********************************************************************
52.19.xxx.xxx : ok=5 changed=0 unreachable=0 failed=0
Who's doing it wrong, me or Ansible? Any ideas?
# ansible --version
ansible 1.9.2

I'm not sure what do you try to achieve, but you exploit an undocumented way of accessing variables via vars hash.
And this vars hash is special in a way that Ansible template engine will not template it, but return it's value as is.
For Ansible 2.x it's described here.
So in case of {{ vars[name]['test_var'] }} it will resolve name->'nested_var' first but will not then resolve vars['nested_var']['test_var'] and return it as literal string.
If your variables are defined as host facts (inventory-host/group variables, dynamic facts set by set_fact), you can access host_vars magic variable like host_vars[inventory_hostname][dynamic_name]['subelement'] to access variable dynamically.
If your variables are play/role bound, like in your case, I can suggest to use root-hash with predefined name, like:
known_name:
nested_var: # this key name is known known in advance
subelement: "{{ 'value-1' if cpu == 'x64' else 'value-2' }}"
Here you can access dynamic element by known_name[dynamic_name]['subelement'].

Related

how does one combine conditionals into one "when" statement?

Ansible 2.10.x
I looked at How to define multiple when conditions in Ansible, and similar posts.
I'm trying to test if 2 different substring are in a variable. I've tried
default/main.yml
----------------
# Default path can be overridden in task
repo_url: "https://someUrl/development"
tasks/main.yml
--------------
- debug:
msg: "URL={{ repo_url }}"
- name: Override default path
set_fact:
repo_url: "https://someUrl/releases"
when: ('"development" not in web_version') and
('"feature" not in web_version')
- debug:
msg: "URL={{ repo_url }}"
I use above task like this for example
$ ansible-playbook ... -e web_version=development_ myTask.yml
But I get
TASK [exa-web : debug] *************************************************
ok: [10.227.x.x] => {
"msg": "URL=https://someUrl/development"
}
TASK [exa-web : Override default path] *************************************************
ok: [10.227.x.x]
TASK [exa-web : debug] *************************************************
ok: [10.227.x.x] => {
"msg": "URL=https://someUrl/releases"
}
I don't expect the set_fact task to run, but it does; hence it overrides the default repo_url. So apparently I'm setting my when condition wrong.
I've also tried this to no avail.
- name: Override default path
set_fact:
repo_url: "https://someUrl/releases"
when: '"development_" not in web_version and
"feature_" not in web_version'
Essentially, I need the task to run if I execute my playbook like this
$ ansible-playbook ... -e web_version=1.4.44 myTask.yml
What's the correct syntax? TIA
UPDATE
Seems like when doesn't like ()? I just simplified the condition for now, and this works
- name: Override default path
set_fact:
repo_url: "https://someUrl/releases"
when: '"development" not in web_version'
but not this?
- name: Override default path
set_fact:
repo_url: "https://someUrl/releases"
when: ('"development" not in web_version')
Really???
Your second attempt...
- name: Override default path
set_fact:
repo_url: "https://someUrl/releases"
when: '"development_" not in web_version and
"feature_" not in web_version'
...seems syntactically correct. In a playbook like this:
- hosts: localhost
gather_facts: false
vars:
repo_url: "https://someUrl/development"
tasks:
- name: Override default path
set_fact:
repo_url: "https://someUrl/releases"
when: '"development_" not in web_version and
"feature_" not in web_version'
- debug:
msg: "URL={{ repo_url }}"
If we run it like this:
ansible-playbook -e web_version=development_ playbook.yaml
We see as output:
TASK [Override default path] ****************************************************************************
skipping: [localhost]
TASK [debug] ********************************************************************************************
ok: [localhost] => {
"msg": "URL=https://someUrl/development"
}
And if we run it like this:
ansible-playbook -e web_version=1.4.44 playbook.yaml
We see:
TASK [Override default path] ****************************************************************************
ok: [localhost]
TASK [debug] ********************************************************************************************
ok: [localhost] => {
"msg": "URL=https://someUrl/releases"
}
That seems to do exactly what you want. Note that you're looking for the string development_ (with a trailing underscore) in your when statement, rather than development as in the first example, but that's an easy fix.
While your code works just fine, I find it helpful to use one of YAML's quote operators for writing multi-line when statements, since it avoids me getting confused by nested quotes in the expression:
- hosts: localhost
gather_facts: false
vars:
repo_url: "https://someUrl/development"
tasks:
- name: Override default path
set_fact:
repo_url: "https://someUrl/releases"
when: >-
"development_" not in web_version and
"feature_" not in web_version
- debug:
msg: "URL={{ repo_url }}"
Re: your update, this doesn't work...
- name: Override default path
set_fact:
repo_url: "https://someUrl/releases"
when: ('"development" not in web_version')
...because of bad quoting. You are effectively writing:
when: ("a string")
And a non-empty string evaluates as true in a boolean expression. Always put the quotes at the beginning of the expression. E.g., this works just fine:
when: >-
("development" not in web_version)
As does the syntactically identical:
when: '("development" not in web_version)'

Ansible variable 'null' vs. 'None'

From what I understood reading about the topic, null and "{{ None }}" are basically the same thing in Ansible. The difference is that the former is language agnostic YAML syntax and the latter is Python specific (I do not speak that language so I do not know how correct that is). However, as the following Ansible playbook shows, they are different.
- hosts: localhost
vars:
a: null
b: "{{ None }}"
tasks:
- name: a always
debug:
var: a
- name: a if none
debug:
var: a
when: a==None
- name: a if not none
debug:
var: a
when: a!=None
- name: b always
debug:
var: b
- name: b if none
debug:
var: b
when: b==None
- name: b if not none
debug:
var: b
when: b!=None
The output from the above is this:
PLAY [localhost] *******************************************************************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************************************************************
ok: [localhost]
TASK [a always] ********************************************************************************************************************************************************************************
ok: [localhost] => {
"a": null
}
TASK [a if none] *******************************************************************************************************************************************************************************
ok: [localhost] => {
"a": null
}
TASK [a if not none] ***************************************************************************************************************************************************************************
skipping: [localhost]
TASK [b always] ********************************************************************************************************************************************************************************
ok: [localhost] => {
"b": ""
}
TASK [b if none] *******************************************************************************************************************************************************************************
skipping: [localhost]
TASK [b if not none] ***************************************************************************************************************************************************************************
ok: [localhost] => {
"b": ""
}
PLAY RECAP *************************************************************************************************************************************************************************************
localhost : ok=5 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
I am not so much concerned about the output null vs. "", but what really bothers me is the different test results a==None compared to b==None.
Can somebody please help me understand what I am missing here?
Regarding
From what I understood reading about the topic, null and "{{ None }}" are basically the same thing in Ansible.
there was a longer conversation under Ansible Issue #7984 "Ansible substitutes YAML null value with "None" in templates".
The difference is that the former is language agnostic YAML syntax and the latter is Python specific
Right, such is discussed under the above mentioned issue.
From the discussion there I understand also that such test is meant for testing defined. And since
To check if it is defined and truthy ... The string "None" evaluates as truthy and defined, the actual python None evaluates as "falsey" and defined.
a test might look like
---
- hosts: localhost
become: false
gather_facts: false
vars:
a: null
b: "{{ None }}"
tasks:
- name: a if none
debug:
var: a
when: a == None
- name: b if none
debug:
var: b
when: b is defined and not b
Further Discussions and Documentation
Ansible Issue #37441 "Ansible incorrectly evaluates 'null' as a variable name in when statements"
Ansible: How to check if a variable is not null?
How to create a null default in Ansible?
Making variables optional
If you are interested more in they types of the variables you may have a look into
---
- hosts: localhost
become: false
gather_facts: false
vars:
a: null
b: "{{ None }}"
c: !!null
tasks:
- name: a if none
debug:
msg: 'a: "{{ a }}" is of {{ a | type_debug }}'
when: a == None
- name: b if none
debug:
msg: 'b: "{{ b }}" is of {{ b | type_debug }}'
when: b is defined and not b
- name: c if none
debug:
msg: "{{ c }}"
when: c == None
resulting into an output of
TASK [a if none] ************
ok: [localhost] =>
msg: 'a: "" is of NoneType'
TASK [b if none] ************
ok: [localhost] =>
msg: 'b: "" is of unicode'
TASK [c if none] ************
ok: [localhost] =>
msg: null

Use dynamic variable name

I'm trying to get the value of ip_address from the following yaml that I'm including as variables on ansible:
common:
ntp:
- time.google.com
node1:
default_route: 10.128.0.1
dns:
- 10.128.0.2
hostname: ip-10-128-5-17
device_interface: ens5
cluster_interface: ens5
interfaces:
ens5:
ip_address: 10.128.5.17
nat_ip_address: 18.221.63.178
netmask: 255.255.240.0
version: 2
However the network interface (ens5 here) may be named something else, such as eth0. My ansible code is this:
- hosts: all
tasks:
- name: Read configuration from the yaml file
include_vars: "{{ config_yaml }}"
- name: Dump Interface Settings
vars:
msg: node1.interfaces.{{ cvp_device_interface }}.ip_address
debug:
msg: "{{ msg }}"
tags: debug_info
Running the code like this I can get the key's name:
TASK [Dump Interface Settings] *************************************************
│ ok: [18.221.63.178] => {
│ "msg": "node1.interfaces.ens5.ip_address"
│ }
But what I actually need is the value (i.e: something like {{ vars[msg] }}, which should expand into {{ node1.interfaces.ens5.ip_address }}). How can I accomplish this?
Use sqare brackets.
Example: a minimal playbook, which defines a variable called "device". This variable is used to return the active status of the device.
- hosts: localhost
connection: local
vars:
device: enx0050b60c19af
tasks:
- debug: var=device
- debug: var=hostvars.localhost.ansible_facts[device].active
Output:
$ ansible-playbook example.yaml
[WARNING]: provided hosts list is empty, only localhost is available. Note that
the implicit localhost does not match 'all'
PLAY [localhost] *******************************************************************
TASK [Gathering Facts] *************************************************************
ok: [localhost]
TASK [debug] ***********************************************************************
ok: [localhost] => {
"device": "enx0050b60c19af"
}
TASK [debug] ***********************************************************************
ok: [localhost] => {
"hostvars.localhost.ansible_facts[device].active": true
}
PLAY RECAP *************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0
see comment
- hosts: all
tasks:
- name: Read configuration from the yaml file
include_vars: "{{ config_yaml }}"
- name: Dump Interface Settings
debug:
msg: "{{ node1['interfaces'][cvp_device_interface]['ip_address'] }}"
debug:
msg: "{{ msg }}"
tags: debug_info

Ansible loop giving warning found a duplicate dict key (when). Using last defined value only

I am trying to iterate over an array and assign the value to variables hooks_enabled, workflow_artifact_id, workflow_version, one by one in every iteration and perform a specific task (currently debug, later change to Helm install command).
Code:
---
- name: Executing Ansible Playbook
hosts: localhost
become: yes
become_user: someuser
pre_tasks:
- include_vars: global_vars.yaml
- name: Print some debug information
set_fact:
all_vars: |
Content of vars
--------------------------------
{{ vars | to_nice_json }}
tasks:
- name: Iterate over an array
set_fact:
hooks_enabled: '{{ array_item1_hooks_enabled }}'
workflow_artifact_id: '{{ array_item1_workflow_artifact_id }}'
workflow_version: '{{ array_item1_workflow_version }}'
when: "item == 'array_item1'"
set_fact:
hooks_enabled: '{{ array_item2_hooks_enabled }}'
workflow_artifact_id: '{{ array_item2_workflow_artifact_id }}'
workflow_version: '{{ array_item2_workflow_version }}'
when: "item == 'array_item2'"
with_items: "{{ array}}"
# Change debug with helm install command
- debug:
msg: " id= '{{ workflow_artifact_id }}'"
The issue I am facing is, only the last when is considered and others are skipped
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
[WARNING]: While constructing a mapping from /c/ansible-test/second.yaml, line 16, column 7, found a duplicate dict key (set_fact). Using last defined value only.
[WARNING]: While constructing a mapping from /c/ansible-test/second.yaml, line 16, column 7, found a duplicate dict key (when). Using last defined value only.
PLAY [Executing Ansible Playbook] *********************************************************************************************************************************************************************************
TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
ok: [localhost]
TASK [include_vars] ***********************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Print some debug information] *******************************************************************************************************************************************************************************
ok: [localhost]
TASK [Iterate over an array] **************************************************************************************************************************************************************************************
skipping: [localhost] => (item=array_item1)
ok: [localhost] => (item=array_item2)
skipping: [localhost] => (item=array_item3)
skipping: [localhost] => (item=array_item4)
skipping: [localhost] => (item=array_item5)
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": " id= 'algorithm-Workflow'"
}
PLAY RECAP ********************************************************************************************************************************************************************************************************
localhost : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
How do I modify the block to enable all the when statement execute and later use helm install command to take the variables one by one.
I would go with a dynamic variable construction using the vars lookup.
Something along the lines of:
- set_fact:
hooks_enabled: "{{ lookup('vars', item ~ '_hooks_enabled') }}"
workflow_artifact_id: "{{ lookup('vars', item ~ '_workflow_artifact_id') }}"
workflow_version: "{{ lookup('vars', item ~ '_workflow_version') }}"
when: "item in ['array_item1', 'array_item2']"
with_items: "{{ array }}"

How can I build a string from a value and a list of values?

[First time questioner. I think that I've targeted this as an Ansible question. If not, gentle redirection is welcome!]
Given:
gid: 80
ports: [80, 443]
where the number of ports may vary from 0 to many
I'd like to produce a string like this:
"gid:80:tcp:80,gid:80:tcp:443"
(which happens to be a FreeBSD mac_portacl rule string)
The furthest I've gotten is:
portacl_rules: "{{ ports | zip_longest([], fillvalue='80') | list }}"
Which gives me somethign like this:
"msg": [
[
80,
"80"
],
[
443,
"80"
]
]
but:
the gid is hardcoded, I can't figure out how to interpolate the variable value; and
I can't translate the list of into the final string.
I can create the gid string, gid:80 by defining a temporary variable:
gid: 80
_tmp_gid: "gid:{{ gid }}"
but since I can't interpolate a string into the fillvalue, I'm stuck.
I monkeyed around the format filter, but it appears to take the output string as its input and the values as its arguments, which is the inverse of my situation.
Any suggestions?
If you don't mind pair of set_fact tasks, you can do it like this:
- set_fact:
rules_list: "{{ rules_list|default([]) + ['gid:{}:tcp:{}'.format(gid, item)] }}"
loop: "{{ ports }}"
- set_fact:
rules_str_1: "{{ ','.join(rules_list) }}"
- debug:
var: rules_str_1
The first task creates a list of the form:
[
"gid:80:tcp:80",
"gid:80:tcp:443"
]
The second task joins those items using ,.
You can complete that in a single operation using a slightly hairier expression involving the regex_replace filter:
- set_fact:
rules_str_2: '{{ ",".join(ports|map("regex_replace", "^(.*)$", "gid:{}:tcp:\1".format(gid))) }}'
- debug:
var: rules_str_2
For that set_fact task to work as written, you must use single quotes on the outside (this inhibits the use of \ as an escape character). You could swap the quotes, but then you would need to write \\ instead of \. Recall that (...) in the match expression creates a capture group, and \1 in the replacement string expands to the value of the first capture group.
Putting it all together in a playbook:
---
- hosts: localhost
gather_facts: false
vars:
gid: 80
ports: [80, 443]
tasks:
- set_fact:
rules_list: "{{ rules_list|default([]) + ['gid:{}:tcp:{}'.format(gid, item)] }}"
loop: "{{ ports }}"
- set_fact:
rules_str_1: "{{ ','.join(rules_list) }}"
- debug:
var: rules_str_1
- set_fact:
rules_str_2: '{{ ",".join(ports|map("regex_replace", "(.*)", "gid:{}:tcp:\1".format(gid))) }}'
- debug:
var: rules_str_2
Which will yield the following output:
PLAY [localhost] ******************************************************************************************************************************************************************************
TASK [set_fact] *******************************************************************************************************************************************************************************
ok: [localhost] => (item=80)
ok: [localhost] => (item=443)
TASK [set_fact] *******************************************************************************************************************************************************************************
ok: [localhost]
TASK [debug] **********************************************************************************************************************************************************************************
ok: [localhost] => {
"rules_str_1": "gid:80:tcp:80,gid:80:tcp:443"
}
TASK [set_fact] *******************************************************************************************************************************************************************************
ok: [localhost]
TASK [debug] **********************************************************************************************************************************************************************************
ok: [localhost] => {
"rules_str_2": "gid:80:tcp:80,gid:80:tcp:443"
}
PLAY RECAP ************************************************************************************************************************************************************************************
localhost : ok=5 changed=0 unreachable=0 failed=0

Resources