How to use a dictionary of registered ansible variables in vars? - ansible

I want to pass multiple variables to a task using vars. Currently, I am doing it like below
vars:
var1_name: "var1_value"
var2_name: "var2_value"
As the number of variables can grow in size, I'd rather prefer to pass the dictionary of variables to the task using vars. I have constructed a dictionary of variables like below
- name: set fact
hosts: localhost
tasks:
- set_fact:
variables: "{{ variables|default({}) | combine( {item.variable: item.value} ) }}"
with_items:
- variable: var1_name
value: "var1_value"
- variable: var2_name
value: "var2_name"
Dictionary looks something like this:
"variables": {
"var1_name": "var1_value",
"var2_name": "var2_value",
}
Now, I want to make variables in this dictionary available to roles executing on other hosts.
But, when I tried to pass dictionary to vars like below
vars: "{{ variables }}"
Ansible throws the error:
ERROR! Vars in a Play must be specified as a dictionary, or a list of dictionaries
How to pass a dictionary variable in vars?

After doing some searching through the Ansible source code, it looks like this is an issue even the developers of Ansible face. In some integration tests, there are specific tests that are commented out because of this same error.
https://github.com/ansible/ansible/blob/devel/test/integration/targets/include_import/role/test_include_role.yml#L96
## FIXME Currently failing with
## ERROR! Vars in a IncludeRole must be specified as a dictionary, or a list of dictionaries
# - name: Pass all variables in a variable to role
# include_role:
# name: role1
# tasks_from: vartest.yml
# vars: "{{ role_vars }}"
I also found out that this is the underlying function that is being called to include the variables:
https://github.com/ansible/ansible/blob/devel/lib/ansible/playbook/base.py#L681
def _load_vars(self, attr, ds):
'''
Vars in a play can be specified either as a dictionary directly, or
as a list of dictionaries. If the later, this method will turn the
list into a single dictionary.
'''
def _validate_variable_keys(ds):
for key in ds:
if not isidentifier(key):
raise TypeError("'%s' is not a valid variable name" % key)
try:
if isinstance(ds, dict):
_validate_variable_keys(ds)
return combine_vars(self.vars, ds)
elif isinstance(ds, list):
all_vars = self.vars
for item in ds:
if not isinstance(item, dict):
raise ValueError
_validate_variable_keys(item)
all_vars = combine_vars(all_vars, item)
return all_vars
elif ds is None:
return {}
else:
raise ValueError
except ValueError as e:
raise AnsibleParserError("Vars in a %s must be specified as a dictionary, or a list of dictionaries" % self.__class__.__name__,
obj=ds, orig_exc=e)
except TypeError as e:
raise AnsibleParserError("Invalid variable name in vars specified for %s: %s" % (self.__class__.__name__, e), obj=ds, orig_exc=e)
Seems as if since "{{ }}" is actually just a YAML string, Ansible doesn't recognize it as a dict, meaning that the vars attribute isn't being passed through the Jinja2 engine but instead being evaluated for what it actually is.
The only way to pass YAML objects around would be to use anchors, however this would require that the object be defined in whole instead of dynamically.
var: &_anchored_var
attr1: "test"
att2: "bar"
vars:
<<: *_anchored_var

I recommend using a structured way of managing variables like:
File myvars1.yml
myvars:
var1_name: "var1_value"
var2_name: "var2_value"
Then read the variables like
- name: Read all variables
block:
- name: Get All Variables
stat:
path: "{{item}}"
with_fileglob:
- "/myansiblehome/vars/common/myvars1.yml"
- "/myansiblehome/vars/common/myvars2.yml"
register: _variables_stat
- name: Include Variables when found
include_vars : "{{item.stat.path}}"
when : item.stat.exists
with_items : "{{_variables_stat.results}}"
no_log : true
delegate_to: localhost
become: false
After that, use like:
- name: My Running Module
mymodule:
myaction1: "{{ myvars.var1_name }}"
myaction2: "{{ myvars.var2_name }}"
Hope it helps

Related

Ansible - set variable based on other item in list

I have configured this list as a variable:
my_list:
- name: whatever
url: http://example.com
- name: dummy
url: http://example.com
Can I configure a variable based on a name in my_list? Something I've tried, but fails because the variable doesn't loop over the list:
other_list:
- a_name: "{{ ip_list.name if my_list.name == 'whatever' }}"
a_url: "{{ ip_list.url if my_list.name == 'whatever' }}"
The expected outcome would be:
other_list:
- a_name: whatever
a_url: http://example.com
I don't want to run Ansible to set the variable during runtime, but I'd rather configure these variable in the variable files.

Not picking up variable in expression correctly - Ansible

I'm trying the following -
---
- name: Test
hosts: "{{ hosts }}"
vars:
before: "groups.{{ hosts[0] }}_group_name"
after: "{{ before }}" # This equals {{ groups.test_group_name }}
roles:
- test-check
Just an explanation: I'm feeding hosts in when executing the playbook as a 'var'. In this case, var = test. The expected var string for before would be groups.test_group_name which is a group that contains multiple hosts in my inventory. However, when I execute this, after remains as groups.test_group_name instead of the expected array of hosts.
Does anybody know how I can remedy this? If I hard-code the host_name (test) into the after var, it picks it up, but if I don't, it doesn't. Thanks.
It appears you are trying to do pseudocode: {{ eval(before) }} but that is not how ansible, or jinja2, work. Thankfully, groups is a normal python dict and thus is subject to the __getitem__ syntax [] to dynamically look up keys
Thus, you likely want:
- hosts: "{{ hosts }}"
vars:
after: "{{ groups[ hosts[0]+'_group_name' ] }}"
tasks:
- debug: var=after

Ansible variable precedence - Reset overriden variable value back to the group_vars value

How Can I reset an overriden variable in a playbook back to the value that is defined in group_vars for that variable
So for example, If I have variable name: "Hello from group_vars" defined in group vars, but during the executin playbook I override it base on some condition with set fact
set_fact:
name: "Overriden somewhere in playbook only for the next task"
when: << some condition that is ture >>
name: Next task
debug:
msg: "Name is {{ name }}" # This will show value from the set_fact
but for the next task I would want to reset the name to the value to the one that has beeen set in group_vars while still in the same playbook execution
set_fact:
name: << How to reset it to group_vars value logic >>
Name: Show the value
debug:
msg: "Nanme is {{ namne }}" # This now should show the value set in group_vars "Hello from group vars"
Any ideas on how to achieve this. Thanks
Generally, once you set_fact for a host, you can't go back, unless you store a copy of the original value and set_fact again later, or re-gather the facts layer (by using a different play in your play book, for example).
If you can, use a specialized fact (and maybe defaults) to achieve a similar goal.
For example:
- name: conditionally set fact
set_fact:
special_name: "overridden value"
when: my_condition
- name: use fact or default
debug:
msg: "Name is {{ special_name | default(name)}}"
If you want to use your overridden value more frequently, you might use a second variable to handle the assignment:
- name: conditionally set fact
set_fact:
special_name: "overridden value"
when: my_condition
- name: use default globally
set_fact:
special_name: "{{ special_name | default(name) }}"
- name: use fact or default
debug:
msg: "Name is {{ special_name }}"
It's a little longer, but it gets you a value you can count on in multiple locations without having to put the default in multiple places.

How to Make Ansible variable mandatory

I'm looking for the way how to make Ansible to analyze playbooks for required and mandatory variables before run playbook's execution like:
- name: create remote app directory hierarchy
file:
path: "/opt/{{ app_name | required }}/"
state: directory
owner: "{{ app_user | required }}"
group: "{{ app_user_group | required }}"
...
and rise error message if variable is undefined, like:
please set "app_name" variable before run (file XXX/main.yml:99)
As Arbab Nazar mentioned, you can use {{ variable | mandatory }} (see Forcing variables to be defined) inside Ansible task.
But I think it looks nicer to add this as first task, it checks is app_name, app_user and app_user_group exist:
- name: 'Check mandatory variables are defined'
assert:
that:
- app_name is defined
- app_user is defined
- app_user_group is defined
You can use this:
{{ variable | mandatory }}
Usually inside a role I perform checking input variables like in the example:
- name: "Verify that required string variables are defined"
assert:
that:
- "{{ ahs_var }} is defined"
- "{{ ahs_var }} | length > 0"
- "{{ ahs_var }} != None"
fail_msg: "{{ ahs_var }} needs to be set for the role to work"
success_msg: "Required variable {{ ahs_var }} is defined"
loop_control:
loop_var: ahs_var
with_items:
- ahs_item1
- ahs_item2
- ahs_item3
by the way there are some tricks:
Don't use global variables inside a role.
If you want use global variables define the role specific variable & set global variable to it i.e. some_role_name__db_port: "{{ db_port | default(5432) }}".
It makes sense to use role name as the prefix for variable. it helps to understand the source inventory easier.
Your role might be looped some how, so it makes sense to override the default item.
One way to check if mandatory variables are defined is:
- fail:
msg: "Variable '{{ item }}' is not defined"
when: item not in vars
with_items:
- app_nam
- var2
There are 2 approaches:
Specify |mandatory filter
Beware dictionaries - if a key with mandatory value fails to eval, you may have a hard time uh understanding which is it.
Use assert module
If using a role-based architecture, check out the docs on role argument validation.
It allows for you to specify a roles/<role_name>/meta/argument_specs.yml file which allows you to make variables required. Here is an example taken from their sample specification:
# roles/myapp/meta/argument_specs.yml
---
argument_specs:
# roles/myapp/tasks/main.yml entry point
main:
short_description: The main entry point for the myapp role.
options:
myapp_int:
type: "int"
required: false
default: 42
description: "The integer value, defaulting to 42."
myapp_str:
type: "str"
required: true
description: "The string value"
# roles/myapp/tasks/alternate.yml entry point
alternate:
short_description: The alternate entry point for the myapp role.
options:
myapp_int:
type: "int"
required: false
default: 1024
description: "The integer value, defaulting to 1024."

Set Ansible variable to undefined through extra-vars or inventory variable

So I have an Ansible playbook that looks like
---
- hosts: mygroup
tasks:
- debug:
msg: "{{ foo | default(inventory_hostname) }}"
My inventory file looks like
[mygroup]
127.0.0.1
Since foo is not defined anywhere, the debug prints 127.0.0.1 as expected.
But suppose my inventory file looks like
[mygroup]
127.0.0.1 foo=null
When I run the playbook, it prints out the string null. I also tried with foo=None and it prints an empty string. How can set the variable to null through inventory or extra-vars?
This may be useful when I want to unset a variable already defined in a playbook.
I am using Ansible version 2.1.1.0.
Python (hence Ansible) differentiates between an undefined variable and a variable with the none value.
There is no way to "undefine" a variable once it has been defined.
In result even if you set the value to none, the condition you specified will never consider the variable as undefined.
You get a "" in the output log because this is how debug module displays the none-value, not because it's an empty string.
Solution 1
Use a ternary operator with the condition to check for the actual value of foo variable:
- debug:
msg: "{{ ((foo is defined) and (foo != None)) | ternary(foo, inventory_hostname) }}"
Solution 2
Use a "wrapper" dictionary:
Define a default value for the variable inside a "wrapper" dictionary:
foodict:
foo: bar
In the play refer the variable as foodict.foo:
---
- hosts: mygroup
tasks:
- debug:
msg: "{{ foodict.foo | default(inventory_hostname) }}"
Override the value in the inventory file by nullifying the "wrapper" dictionary:
[mygroup]
127.0.0.1 foodict=None

Resources