How to retrieve dictionary value when creating key from set_fact - dictionary

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 "{{ ...}}"

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') }}"

Fail to get values from dictionary in ansible playbook

I'm trying to get some values from a dictionary in ansible playbook.
First, I build the dictionary and it runs correctly.
Then, I would compute the output_dict evaluating the values of this dictionary but I have the issue above.
- set_fact:
state_service: "{{ state_service | default({}) | combine({ item.key : item.value }) }}"
with_items:
- { 'key': 'op_state', 'value': "{{Check_Operative_State.results[0].stdout}}"}
- { 'key': 'ad_state', 'value': "{{Check_Administrative_State.results[0].stdout}}"}
- name: Display the state of service
debug: msg="{{state_service}}"
- set_fact:
output_dict: '{{output_dict | default([]) + ["CHECK ERRORS IN SERVICE ************PASSED" if ((item.op_state == "enabled" and item.ad_state == "unlocked") or (item.op_state == "disabled" and item.ad_state == "locked")) else "CHECK ERRORS IN SERVICE **********FAILED"] }}'
with_items: "{{state_service}}"
The error:
**************************************************
2022-02-04 19:04:45,341 p=4478 u=abcd n=ansible | ok: [localhost] => {
"msg": {
"ad_state": "unlocked",
"op_state": "enabled"
}
}
2022-02-04 19:04:45,366 p=4478 u=abcd n=ansible | TASK [check : set_fact] ************************************************************************************************************************************************************************
2022-02-04 19:04:45,366 p=4478 u=abcd n=ansible | fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'ansible.utils.unsafe_proxy.AnsibleUnsafeText object' has no attribute 'op_state'\n\nThe error appears to be in '/home/abcd/health/service/script/roles/health/tasks/main.yaml': line 483, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- set_fact:\n ^ here\n"}
2022-02-04 19:04:45,368 p=4478 u=abcd n=ansible | PLAY RECAP *****************************************************************************************************************************************
Given the list below to simplify the example
l1:
- {key: op_state, value: enabled}
- {key: ad_state, value: unlocked}
Use the filter items2dict to create a dictionary from the list, e.g.
state_service: "{{ l1|items2dict }}"
gives
state_service:
ad_state: unlocked
op_state: enabled
You can't iterate a dictionary, but you can use dict2items to convert the dictionary to a list, e.g.
- debug:
var: item
loop: "{{ state_service|dict2items }}"
gives (abridged)
item:
key: op_state
value: enabled
item:
key: ad_state
value: unlocked

How to create a dynamic dictionary in Ansible

I have a list of users (this is not actual Ansible code, just an example):
users:
-tom
-jerry
-pluto
Now I am trying to create a dynamic structure ( list of dictionaries ?) like this, for setting random (at runtime) passwords to them:
users_and_passwords:
-tom, "random_password_here1"
-jerry, "random_password_here2"
-pluto, "random_password_here3"
How can I write a set_fact task to generate random password for each user and save it for later use?
How can I then read that and use it in other tasks later?
So far I tried declaring a list of users (later to be looped from a file), then an empty list of passwords. Now I am trying to populate the Password list with random strings, then to combine them into a single {user:password} dict.I can't figure out how to do this easier, and I am stuck in the "populate password list" phase.
users:
-top
-jerry
-pluto
passwords: []
tasks:
- name: generate passwords in an empty Dict
set_fact:
passwords: "{{ passwords | default({}) + {{ lookup('password', '/dev/null chars=ascii_lowercase,ascii_uppercase,digits,punctuation length=20') }} }}"
with_items: "{{users}}"
Rather than having a complex dictionary that would read:
- tom: password123456
- jerry: password123456789
- muttley: password12345
Aim for a structure that would be normalized, like:
- name: tom
password: password123456
- name: jerry
password: password123456789
- name: muttley
password: password12345
This can be achieved with the task:
- set_fact:
users_with_passwords: >-
{{
users_with_passwords | default([]) +
[{
'name': item,
'password': lookup(
'password',
'/dev/null chars=ascii_lowercase,ascii_uppercase,digits,punctuation length=20'
)
}]
}}
loop: "{{ users }}"
And, can be easily accessed via something like:
- debug:
msg: "{{ item.name }}'s password is `{{ item.password }}`"
loop: "{{ users_with_passwords }}"
loop_control:
label: "{{ item.name }}"
Given the playbook
- hosts: localhost
gather_facts: no
vars:
users:
- tom
- jerry
- muttley
tasks:
- set_fact:
users_with_passwords: >-
{{
users_with_passwords | default([]) +
[{
'name': item,
'password': lookup(
'password',
'/dev/null chars=ascii_lowercase,ascii_uppercase,digits,punctuation length=20'
)
}]
}}
loop: "{{ users }}"
- debug:
msg: "{{ item.name }}'s password is `{{ item.password }}`"
loop: "{{ users_with_passwords }}"
loop_control:
label: "{{ item.name }}"
This yields:
TASK [set_fact] *************************************************************
ok: [localhost] => (item=tom)
ok: [localhost] => (item=jerry)
ok: [localhost] => (item=muttley)
TASK [debug] ****************************************************************
ok: [localhost] => (item=tom) =>
msg: tom's password is `tLY>#jg6k/_|sqke{-mm`
ok: [localhost] => (item=jerry) =>
msg: jerry's password is `Liu1wF#gPM$q^z~g|<E1`
ok: [localhost] => (item=muttley) =>
msg: muttley's password is `BGHL_QUTHmbn\(NGW`pJ`
How can I write a set_fact task to generate random password for each user and save it for later use?
This sound like a task for the password_lookup which "Retrieves or generate a random password, stored in a file", in Example
- name: Create random but idempotent password
set_fact:
toms_password: "{{ lookup('password', '/dev/null', seed=username) }}"
How can I then read that and use it in other tasks later?
Just by Using variable name which was defined during set_fact.
Further readings
Ansible documentation Lookup plugins
How to generate single reusable random password with Ansible
Ansible: Generate random passwords automatically for users
Creating user passwords from an Ansible Playbook
How do I create a user and set a password using Ansible

Dictionary value access using ansible

I have a situation where we have 2 dictionary defined in ansible role default and the selection of dictionary is based of an input variable. I want to set the fact with one of the dict's specific key value.
Below is the example code:
test.yml paybook content:
- hosts: localhost
gather_facts: true
roles:
- role1
tags: ['role1']
roles/role1/tasks/main.yml content:
- name: set fact
set_fact:
node_vip: "{% if node_vip_run == 'no' %}node_vip_no{% elif node_vip_run == 'yes' %}node_vip_yes{% endif %}"
- debug:
var: node_vip
verbosity: 1
- debug:
var: "{{ node_vip }}.ece_endpoint"
verbosity: 1
- name: set fact
set_fact:
ece_endpoint_fact: "{{ node_vip[ece_endpoint] }}"
- debug:
var: ece_endpoint
verbosity: 1
roles/role1/defaults/main.yml content:
node_vip_yes:
ece_endpoint: "https://1.1.1.1:8080"
cac_endpoint: "https:2.2.2.2:8080"
node_vip_no:
ece_endpoint: "http://3.3.3.3:8080"
cac_endpoint: "http:4.4.4.4:8080"
Run playbook:
ansible-playbook test.yaml --extra-vars 'node_vip_run=no' -v
The set fact of variable "ece_endpoint_fact" should have value "https://1.1.1.1:8080 OR http://3.3.3.3:8080" depending on the parameter input in ansible command. But I keep on getting below error:
TASK [role1 : set fact] *******************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'unicode object' has no attribute u'http://3.3.3.3:8080'\n\nThe error appears to be in '/root/roles/role1/tasks/main.yml': line 46, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- name: set fact\n ^ here\n"}
Please suggest what needs to be done to resolve this.
Thanks
Right now, you set node_vip to either the literal string "node_vip_no" or "node_vip_yes". But if you change it to do {{ node_vip_no }} / {{ node_vip_yes }}, then node_vip will have the value of the variable node_vip_no / node_vip_yes instead of being a literal string.
- name: set fact
set_fact:
node_vip: "{% if node_vip_run == 'no' %}{{ node_vip_no }}{% elif node_vip_run == 'yes' %}{{ node_vip_yes }}{% endif %}"
This will have node_vip's value be something like:
TASK [debug] ***************************************************************
ok: [localhost] => {
"node_vip": {
"cac_endpoint": "https:2.2.2.2:8080",
"ece_endpoint": "https://1.1.1.1:8080"
}
}
Then in your other set_fact, it should work if you put quotes around the property name:
- name: set fact
set_fact:
ece_endpoint_fact: "{{ node_vip['ece_endpoint'] }}"
# Added quotes ^ ^

ansible changing nested dict variable

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.

Resources