ansible changing nested dict variable - dictionary

After deployment of a VM with a DHCP IP I would like to get the IP and append it to the guests dictionary.
For the first VM (testvm2) the code perfoms as expected and updates the tempip variable for the testvm2 VM.
But with the second VM (testvm1), it updates the tempip variable of the first VM (testvm2) with the IP of the second VM (testvm1), and updates the tempip variable of the second VM (testvm1) with the code of the variable '{{ tempip_reg.stdout_lines }}'
Can anyone explain to me why this happens?
I would appreciate the help.
I copied all the relevant code and output below:
guests dictionary:
---
guests:
testvm1:
mem: 512
cpus: 1
clone: template-centos
vmid: 102
tempip:
testvm2:
mem: 1536
cpus: 2
clone: template-centos
vmid: 102
tempip:
Ansible Playbook that starts the task:
---
- name: Provision VMs
hosts: infra
become: true
vars_files:
- group_vars/vms.yml
- group_vars/vars.yml
tasks:
- include_tasks: roles/tasks/provision-tasks.yml
with_dict: "{{ guests }}"
Ansible Tasks:
- name: Run proxmox-get-ip-linux.sh script to register DHCP IP of VM
script: proxmox-get-ip-linux.sh "{{ item.key }}"
register: tempip_reg
- name: temporary IP of VM "{{ item.key }}"
debug:
var: tempip_reg
- name: current host in item.key
set_fact:
current_host: "{{ item.key }}"
- name: current_host variable set to
debug:
var: current_host
- name: append item.value.tempip with the DHCP IP of the VM registered in
last task
set_fact:
guests: "{{ guests|combine({ current_host: {'tempip': '{{ tempip_reg.stdout_lines }}' }}, recursive=True) }}"
- name: temporary IP of "{{ item.key }}"
debug: var=guests
Result first VM:
"tempip_reg": {
"stdout": "192.168.1.21\r\n",
"stdout_lines": [
"192.168.1.21"
}
"current_host": "testvm2"
"guests": {
"testvm1": {
"clone": "template-centos",
"cpus": 1,
"ip": "192.168.1.60",
"mem": 512,
"tempip": null,
"vmid": 102
},
"testvm2": {
"clone": "template-centos",
"cpus": 2,
"ip": "192.168.1.61",
"mem": 1536,
"tempip": [
"192.168.1.21"
],
"vmid": 102
}
}
Result 2nd VM:
"tempip_reg": {
"stdout": "192.168.1.22\r\n",
"stdout_lines": [
"192.168.1.22"
}
"current_host": "testvm1"
"guests": {
"testvm1": {
"clone": "template-centos",
"cpus": 1,
"ip": "192.168.1.60",
"mem": 512,
"tempip": "{{ tempip_reg.stdout_lines }}",
"vmid": 102
},
"testvm2": {
"clone": "template-centos",
"cpus": 2,
"ip": "192.168.1.61",
"mem": 1536,
"tempip": [
"192.168.1.22"
],
"vmid": 102
}
}

TL;DR
Using Ansible code, you are trying to implement what Ansible already does for you.
Your attempts superimpose with built-in functionality and you get results which look nondeterministic.
Explanation:
The main problem with your code is a completely unnecessary loop declared with with_dict: "{{ guests }}" which causes to include the file 4 times.
It runs 4 times because you change the guests dictionary, which it loops over inside the included tasks-file.
In effect you get something which looks like an nondeterministic result.
The second problem is a trivial one: you always replace the value of tempip with a string {{ tempip_reg.stdout_lines }}.
Now, because of the unnecessary with_dict loop over a dictionary which you dynamically change, and because Jinja2 uses lazy variable evaluation, strings from previous iterations are interpreted as templates and get evaluated with incorrect values in subsequent iterations.
The last iteration leaves the string {{ tempip_reg.stdout_lines }} intact.
You also define and print two different facts.
What you should do:
You should not declare arbitrary iterations at all. Ansible implements a loop for all hosts itself. That is, if you declare a task:
- include_tasks: roles/tasks/provision-tasks.yml
the file will be included for each of the hosts in infra group (twice in your example).
You seem to want to have a single copy of your data structure with updated values for each VM.
At the same time, you create a fact, which is a separate data object maintained for each host separately.
So you should refer to and modify (combine) a single fact - you can do it for example on localhost.
You should structure your code like this:
---
- name: Provision VMs
hosts: infra
become: true
vars_files:
- group_vars/vms.yml
- group_vars/vars.yml
tasks:
- include_tasks: roles/tasks/provision-tasks.yml
- debug:
var: hostvars['localhost'].guests
and provision-tasks.yml:
- set_fact:
guests: "{{ guests|combine({ current_host: {'tempip': tempip_reg.stdout_lines }}, recursive=True) }}"
delegate_to: localhost
This will get you the following result:
"hostvars['localhost'].guests": {
"testvm1": {
"clone": "template-centos",
"cpus": 1,
"ip": "192.168.1.60",
"mem": 512,
"tempip": [
"192.168.1.21"
],
"vmid": 102
},
"testvm2": {
"clone": "template-centos",
"cpus": 2,
"ip": "192.168.1.61",
"mem": 1536,
"tempip": [
"192.168.1.22"
],
"vmid": 102
}
}
Finally, in the above play, you used group_vars and roles/tasks directories in wrong context. I left the paths intact and they will work for the above code, but basically you should never use them this way, because again, they have special meaning and treatment in Ansible.

Related

Error "Vars in a Task must be specified as a dictionary, or a list of dictionaries"

'data_list' consists of the values in the csv file. I want to use the values in 'data_list' to loop through the parameters in the 'Create user' section of the playbook, but I am getting this error after running my playbook:
TASK [Create Multiple Users : Create multiple users] ***************************
fatal: [10.16.220.30]: FAILED! => {"reason": "Vars in a Task must be specified as a dictionary, or a list of dictionaries\n\nThe error appears to be in '/runner/project/Windows AD/roles/Create Multiple Users/tasks/Create_multiple_users.yml': line 14, column 9, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n - \"{{ item.groups }}\"\n vars: data_list\n ^ here\n"}
This is my playbook:
---
- name: Read Users
hosts: localhost
vars:
data_list: []
tasks:
- read_csv:
path: user.csv
key: name
fieldnames: name,firstname,surname,displayName,groups
delimiter: ','
register: userdata
- name: Extract the list
set_fact:
data_list: "{{ data_list + [{ 'name': item.value.name, 'firstname': item.value.firstname, 'surname': item.value.surname, 'displayName': item.value.displayName, 'groups': item.value.groups }] }}"
loop: "{{ userdata.dict|dict2items }}"
- name: Create user accounts
hosts: "{{ hostname }}"
gather_facts: false
any_errors_fatal: false
become: yes
become_method: runas
become_user: admin
roles:
- { role: Create Multiple Users }
- name: Create users
community.windows.win_domain_user:
name: "{{ item.name }}"
firstname: "{{ item.firstname }}"
surname: "{{ item.surname }}"
attributes:
displayName: "{{ item.firstname + ' ' + item.surname }}"
groups:
- "{{ item.groups }}"
vars: data_list
with_items:
- "{{ data_list }}"
What is the correct vars that I should write?
This is the line causing the error in your task
vars: data_list
As mentioned in your error message, the vars section should look like:
vars:
var1: value1
var2: value2
But this is not the only problem in you above script. You are gathering your csv data in a separate play on localhost and setting that info as a fact in variable data_list. When your first play is over, that var will only be known from the localhost target. If you want to reuse it in a second play targeting other hosts, you'll have to get that var from the hostvars magic variable
{{ hostvars.localhost.data_list }}
This is not the best approach here as you can easily shorten your playbook to a single play. The trick here is to delegate your csv gathering task to localhost and set run_once: true so that the registered var is calculated only once and distributed to all hosts with the same value. You can also drop the set fact which basically copies the same key: value to a new var.
Here is an (untested) example playbook to show you the way:
---
- name: Create multiple Windows AD user accounts from CSV
hosts: "{{ hostname }}"
gather_facts: false
tasks:
- name: read csv from localhost (single run same registered var for all hosts)
read_csv:
path: user.csv
key: name
fieldnames: name,firstname,surname,displayName,groups
delimiter: ','
register: userdata
run_once: true
delegate_to: localhost
- name: Create users
community.windows.win_domain_user:
name: "{{ item.name }}"
firstname: "{{ item.firstname }}"
surname: "{{ item.surname }}"
attributes:
displayName: "{{ item.firstname + ' ' + item.surname }}"
groups:
- "{{ item.groups }}"
# This will work on any number of hosts as `userdata`
# now has the same value for each hosts inside this play.
# we just have to extract the values from each keys from
# `userdata` and loop on that list
loop: "{{ userdata.dict | dict2items | map(attribute='value') }}"

How to retrieve dictionary value when creating key from set_fact

I am trying to retrieve a value from a dictionary by 'dotting' through the keys with a variable (using set_fact).
How do I retrieve the value for applications.office.nuspec.id if i built it through set_fact?
Here is my dictionary
vars:
applications:
office:
nuspec:
id: data_wanted
Here is the code put together with "current_chocolatey_parameter_value" storing the location of the dictionary value I want
- name: Set variable to id
set_fact: selected_current_chocolatey_parameter=id
- name: Create string to represent variable to select value from dictionary
set_fact: current_chocolatey_parameter_value="applications.office.nuspec.{{ selected_current_chocolatey_parameter }}"
- name: The combined new string is
debug: msg="{{ current_chocolatey_parameter_value }}"
TASK [The combined new string is]
******************************************************************************************
***********************************************************
ok: [localhost] => {
"msg": "applications.office.nuspec.id"
}
I have tried lookup('vars', current_chocolatey_parameter_value) with no luck.
How do I get the value of applications.office.nuspec.id from my defined dictionary when I have the string stored in a variable that I want to reference it with?
---
- name: Test lookup play
hosts: localhost
connection: local
gather_facts: false
vars:
applications:
office:
nuspec:
id: data_wanted
tasks:
- name: Set variable to id
set_fact: selected_current_chocolatey_parameter=id
- name: Create string to represent variable to select value from dictionary
set_fact: current_chocolatey_parameter_value="applications.office.nuspec.{{ selected_current_chocolatey_parameter }}"
- name: The combined new string is
debug: msg="{{ current_chocolatey_parameter_value }}"
- name: Looked up value 1
debug: msg="{{ applications.office.nuspec[selected_current_chocolatey_parameter] }}"
- name: Looked up value 2
debug: msg="{{ applications.office.nuspec.get(selected_current_chocolatey_parameter) }}"
Produces
$ ansible-playbook -i localhost, stackoverflow1.yml
PLAY [Test lookup play] *******************************************************************************
TASK [Set variable to id] *****************************************************************************
ok: [localhost]
TASK [Create string to represent variable to select value from dictionary] ****************************
ok: [localhost]
TASK [The combined new string is] *********************************************************************
ok: [localhost] => {
"msg": "applications.office.nuspec.id"
}
TASK [Looked up value 1] ******************************************************************************
ok: [localhost] => {
"msg": "data_wanted"
}
TASK [Looked up value 2] ******************************************************************************
ok: [localhost] => {
"msg": "data_wanted"
}
Remember that you can call any core Python functions between "{{ ...}}"

Iterate through a dictionary with subelements in Ansible

I have to create a list based on a dictionary.
To get the element from the dictionary, I need to join "the server" + "the domain". The problem is that I have 3 different domains.
At the moment, I'm repeating the code to be able to use the 3 different domains.
- name: "Get server instances {{ ansible_fqdn }}"
set_fact:
app_ps_mon_list: "{{ app_ps_mon_list | default ([]) + [ app_instance ] }}"
vars:
app_instance: |
[Java,<event_type>]
java critical 1-
*ARGS {{item.value.INSTANCE_NAME.split("/")[1]}}
with_dict: '{{ server_instances[ansible_hostname + "<DOMAIN1>"] }}'
when: '{{server_instances[ansible_hostname + "<DOMAIN1>"] is defined and item.key != "SERVER_IMPACT"}}'
- name: "Get server instances {{ ansible_fqdn }}"
set_fact:
app_ps_mon_list: "{{ app_ps_mon_list | default ([]) + [ app_instance ] }}"
vars:
app_instance: |
[Java,<event_type>]
java critical 1-
*ARGS {{item.value.INSTANCE_NAME.split("/")[1]}}
with_dict: '{{ server_instances[ansible_hostname + "<DOMAIN2>"] }}'
when: '{{server_instances[ansible_hostname + "<DOMAIN2>"] is defined and item.key != "SERVER_IMPACT"}}'
- name: "Get server instances {{ ansible_fqdn }}"
set_fact:
app_ps_mon_list: "{{ app_ps_mon_list | default ([]) + [ app_instance ] }}"
vars:
app_instance: |
[Java,<event_type>]
java critical 1-
*ARGS {{item.value.INSTANCE_NAME.split("/")[1]}}
with_dict: '{{ server_instances[ansible_hostname + "<DOMAIN3>"] }}'
when: '{{server_instances[ansible_hostname + "<DOMAIN3>"] is defined and item.key != "SERVER_IMPACT"}}'
I've been trying to do the same with_subelements without success. Also I tried to use "ansible_fqdn", but the fqdn domain usually don't match the actual domain (I know its a mess).
Is there any workaround I could use to avoid repeating the code?
UPDATE
The idea of the playbook is to obtain the solutions from the server I'm using as host (ansible_hostname varaible).
Once it get the solutions, create a list using some information from the INSTANCE_NAME
This is a generic version of the dictionary:
{
"<server_name><domain>": {
"<solution_id>": {
"INSTANCE_NAME": "",
"SOLUTION_CATEGORY": "",
"SOLUTION_NAME": ""
},
"<solution_id>": {
"INSTANCE_NAME": "",
"SOLUTION_CATEGORY": "",
"SOLUTION_NAME": ""
},
SERVER_IMPACT: "",
...,
}
This is how the list should look like (its a multiline string variable, dumb but useful):
[
[Java]
java critical 1-
*ARGS <iINSTANCE_NAME info>
],
[
[Java]
java critical 1-
*ARGS <iINSTANCE_NAME info>
]

Combine nested dictionaries in ansible

I have 2 different dictionaries that contains application information I need to join together.
landscape_dictionary:
{
"app_1": {
"Category": "application",
"SolutionID": "194833",
"Availability": null,
"Environment": "stage",
"Vendor/Manufacturer": null
},
"app_2": false
}
app_info_dictionary:
{
"app_1": {
"app_id": "6886817",
"owner": "owner1#nomail.com",
"prod": [
"server1"
],
"stage": []
},
"app_2": {
"app_id": "3415012",
"owner": "owner2#nomail.com",
"prod": [
"server2"
],
"stage": [
"server3"
]
}
}
This is the code I'm using to join both dictionaries
- set_fact:
uber_dict: "{{app_info_dictionary}}"
- set_fact:
uber_dict: "{{ uber_dict | default ({}) | combine(new_item, recursive=true) }}"
vars:
new_item: "{ '{{item.key}}' : { 'landscape': '{{landscape_dictionary[item.key]|default(false)}}' } }"
with_dict: "{{ uber_dict }}"
- debug:
msg: "{{item.key}}: {{item.value}}"
with_dict: "{{uber_dict}}"
If the value in the landscape_dictionary is false it will add it to the uber_dict without problems. But if the value contains information, it fails.
This is the error:
fatal: [127.0.0.1]: FAILED! => {"msg": "|combine expects dictionaries, got u\"{ 'app_1' : { 'landscape': '{u'Category': u'application', u'SolutionID': u'194820', u'Availability': None, u'Environment': 'stage', u'Vendor/Manufacturer': None}' } }\""}
What could be the problem?
Do I need to do an extra combine when I set the var in the set_fact?
Thanks
As #DustWolf notes in the comments,
For anyone from the Internet looking for the answer to: "How tp combine nested dictionaries in ansible", the answer is | combine(new_item, recursive=true)
This solves a closely related issue that has baffled myself and my team for months.
I will demonstrate:
Code:
---
- hosts: localhost
gather_facts: false
vars:
my_default_values:
key1: value1
key2:
a: 10
b: 20
my_custom_values:
key3: value3
key2:
a: 30
my_values: "{{ my_default_values | combine(my_custom_values, recursive=true) }}"
tasks:
- debug: var=my_default_values
- debug: var=my_values
Output:
ok: [localhost] =>
my_values:
key1: value1
key2:
a: 30
key3: value3
Note how key2 was completely replaced, thus losing key2.b
We changed this to:
my_values: "{{ my_default_values | combine(my_custom_values, recursive=true) }}"
Output:
my_values:
key1: value1
key2:
a: 30
b: 20
key3: value3
This syntax is not legal, or at the very least doesn't do what you think:
new_item: "{ '{{item.key}}' : { 'landscape': '{{landscape_dictionary[item.key]|default(false)}}' } }"
Foremost, ansible will only auto-coerce JSON strings into a dict, but you have used python syntax.
Secondarily, the way to dynamically construct a dict is not to use jinja2 to build up text but rather use the fact that jinja2 is almost a programming language:
new_item: "{{
{
item.key: {
'landscape': landscape_dictionary[item.key]|default(false)
}
}
}}"
Any time you find yourself with nested jinja2 interpolation blocks, that is a code smell that you are thinking about the problem too much as text (by nested, I mean {{ something {{nested}} else }})

Create a list of dictionaries from host groups in ansible playbook

I want to dynamically create a list of dictionaries that looks like this:
[ {'host': 'hostname1', 'id': 1}, {'host': 'hostname2', 'id': 2}, ]
And assign it to a variable in my playbook.
This variable is needed for a role I am using.
My attempt is the following:
- hosts:
- some-hosts
vars:
zk_hosts: []
tasks:
- name: create my var
set_fact:
zk_hosts: "{{ zk_hosts + [ {'host': item.1, 'id': item.0} ] }}"
with_indexed_items: "{{ groups.some-hosts }}"
However, when I run the playbook I have this warning:
[WARNING]: While constructing a mapping from stack.yml, line 16, column 3, found a duplicate dict key (vars). Using last defined value only.
And and error at this play:
fatal: [192.168.0.21]: FAILED! => {"failed": true, "msg": "ERROR! 'zk_hosts' is undefined"}
If I don't define zk_hosts before trying to set the fact, I get an error that the variable is undefined.
How can I solve?
EDIT
Easy fix, I just defined zk_hosts within the same task...
tasks:
- name: create my var
vars:
zk_hosts: []
set_fact:
zk_hosts: "{{ zk_hosts + [ {'host': item.1, 'id': item.0} ] }}"
with_indexed_items: "{{ groups.some-hosts }}"
Anyway, if there is a less cumbersome way of achieving the same, please advise!
You can use default filter:
set_fact:
zk_hosts: "{{ zk_hosts|default([]) + [ {'host': item.1, 'id': item.0} ] }}"
with_indexed_items: "{{ groups.some-hosts }}"

Resources