Best way to conditionally override an item value in an Ansible loop - loops

I'm attempting to adapt an existing play that was built for automating account creation and deletion in a small environment without centralized authentication. Since then the scope has expanded and we've added bastion servers where it's not appropriate for all accounts to be created.
Because user IDs are hard coded (not desirable - I know) I'd like to avoid having a separate play for the bastions as that means keeping track of UIDs in two places. My workaround was to add an onbastion boolean to each row, then use that in create-account.yml to flip the state to absent if applicable. I originally tried changing the value of item.state directly but ran into syntax errors that I couldn't figure out. So I added a set_fact task to set the value of a new variable conditionally, but the playbook now fails with a message that "The task includes an option with an undefined variable. The error was: 'desired_item_state' is undefined".
What am I missing, and is there a better way to tackle this? Thanks in advance!
Relevant Snippets
project/roles/create-accounts/default/main.yml
users:
- { name: "user_1", id: 15001, state: "present", groups: ["project"], onbastion: true }
- { name: "user_2", id: 15002, state: "present", groups: ["project"], onbastion: true }
- { name: "fired_user_3", id: 15003, state: "absent", groups: [], onbastion: false}
- { name: "retired_user_4", id: 15004, state: "absent", groups: [], onbastion: false}
- { name: "osr1", id: 15009, state: "present", groups: ["project"], onbastion: false }
- { name: "osr2", id: 15010, state: "present", groups: ["project"], onbastion: false }
- { name: "osr3", id: 15011, state: "present", groups: ["project"], onbastion: false }
- { name: "project_svc", id: 15018, state: "present", groups: ["project"], onbastion: false }
- { name: "jenkins", id: 15019, state: "present", groups: ["project"], onbastion: false }
project/roles/create-accounts/tasks/main.yml
- name: Set up user accounts
ansible.builtin.include_tasks: create-account.yml
loop: "{{ users }}"
project/roles/create-accounts/tasks/create-account.yml
---
- name: Update item state if on bastion
set_fact:
desired_item_state: "{{ 'absent' if ('bastion' in group_names and item.onbastion == false) else item.state }}"
- name: Create/update {{ item.name }} user ID
become: true
ansible.builtin.user:
name: "{{ item.name }}"
state: "{{ desired_item_state }}"
uid: "{{ item.id }}"
groups: "{{ item.groups }}"
append: yes
update_password: on_create
password: "!"
- name: Create/update {{ item.name }} group ID
become: true
ansible.builtin.group:
name: "{{ item.name }}"
state: "{{ desired_item_state }}"
gid: "{{ item.id }}"
- name: Update ownership of /home/{{ item.name }}
become: true
ansible.builtin.file:
path: /home/{{ item.name }}
state: directory
recurse: yes
owner: "{{ item.name }}"
group: "{{ item.name }}"
when: desired_item_state == 'present'
Output
TASK [create-accounts : Create/update osr1 user ID] ***********************
fatal: [project-subsystem-bastion2]: FAILED! =>
msg: |-
The task includes an option with an undefined variable. The error was: 'desired_item_state' is undefined
The error appears to be in '/project/ansible-e86f3754/plays/roles/create-accounts/tasks/create-account.yml': line 13, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: Create/update {{ item.name }} user ID
^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value. For instance:
with_items:
- {{ foo }}
Should be written as:
with_items:
- "{{ foo }}"

See comments. flowerysong was unable to reproduce my issue, and the only thing I'd left out were tags on the tasks. Lesson learned: sanitize code before posting but don't leave anything out.

Related

Construct a Distinguished Name with Ansible

In my ansible playbook I have a variable defining the OU DN I'd like to create.
my_ou: "OU=win10,OU=workstations,OU=mycompany"
domain_root: "DC=example,DC=com"
I'd like to convert my_ou to a list and create each OU. The problem is that the loop does not know the path for each OU because it is an aggregation of the previous elements in the loop.
If I split my_ou on ',' and reverse the loop, I can print each OU in order of creation. I can set the name by using regex_replace. However, I don't know how to construct the path.
For example:
- name: create required OU's
win_domain_ou:
name: "{{ my_ou | regex_replace('^OU=') }}"
path: ???? should be all items to the right of the current {{ item }},{{ domain_root }}
state: present
loop: "{{ my_ou.split(',') | reverse | list }}"
So, to be clear, the path for OU=win10 should be OU=workstations,OU=mycompany,DC=example,DC=com
The OU=workstations path should be OU=mycompany,DC=example,DC=com
And so forth.
Thanks!
Declare the list of elements
elements: "{{ my_ou.split(',') + domain_root.split(',') }}"
gives
elements:
- OU=win10
- OU=workstations
- OU=mycompany
- DC=example
- DC=com
and use extended loop variables. For example,
- debug:
msg: "item: {{ item }} path: {{ elements[idx|int:]|join(',') }}"
loop: "{{ my_ou.split(',')|reverse }}"
loop_control:
extended: true
vars:
idx: "{{ my_ou.split(',')|length - ansible_loop.index0 }}"
gives (abridged)
msg: 'item: OU=mycompany path: DC=example,DC=com'
msg: 'item: OU=workstations path: OU=mycompany,DC=example,DC=com'
msg: 'item: OU=win10 path: OU=workstations,OU=mycompany,DC=example,DC=com'
Example of a complete playbook for testing
- hosts: localhost
vars:
my_ou: "OU=win10,OU=workstations,OU=mycompany"
domain_root: "DC=example,DC=com"
elements: "{{ my_ou.split(',') + domain_root.split(',') }}"
tasks:
- debug:
msg: "item: {{ item }} path: {{ elements[idx|int:]|join(',') }}"
loop: "{{ my_ou.split(',')|reverse }}"
loop_control:
extended: true
vars:
idx: "{{ my_ou.split(',')|length - ansible_loop.index0 }}"

Ansible: How to loop through 2 var dicts only when hostname corresponds

Im trying to loop to a set of variables depending on the hostname. I cant get the conditional right.
Say i have 2 set of vars:
databases_eu:
- name: database_eu_1
port: 5432
login: {{ vault_user_database_eu_1 }}
password: {{ vault_pw_database_eu_1 }}
- name: database_eu_2
port: 5433
login: {{ vault_user_database_eu_2 }}
password: {{ vault_pw_database_eu_2 }}
databases_us:
- name: database_us_1
port: 5432
login: {{ vault_user_database_us_1 }}
password: {{ vault_pw_database_us_1 }}
- name: database_us_2
port: 5433
login: {{ vault_user_database_us_2 }}
password: {{ vault_pw_database_us_2 }}
And i want to loop through it depending on the hostname, how would i do that?
I thought something like below, but im missing the when statement:
- name: copy configs
ansible.builtin.template:
src: config.yml.j2
dest: /opt/db/configs/{{ item.name }}.yml
owner: root
group: root
mode: '0644'
loop:
- "{{ databases_eu | inventory_hostname == 'eu.db.example.com' }}"
- "{{ databases_us | inventory_hostname == 'us.db.example.com' }}"
Use the lookup plugin vars to get the value of a variable. See
shell> ansible-doc -t lookup vars
There are many options on how to reference the data. For example, given the inventory
shell> cat hosts
eu.db.example.com
us.db.example.com
Create a dictionary of data assigned to the hosts, e.g.
databases_host:
eu.db.example.com: databases_eu
us.db.example.com: databases_us
Then the playbook
- hosts: all
tasks:
- debug:
msg: "Configure {{ item.name }}"
loop: "{{ lookup('vars', databases_host[inventory_hostname]) }}"
loop_control:
label: "{{ item.name }}"
gives
ok: [eu.db.example.com] => (item=database_eu_1) =>
msg: Configure database_eu_1
ok: [us.db.example.com] => (item=database_us_1) =>
msg: Configure database_us_1
ok: [us.db.example.com] => (item=database_us_2) =>
msg: Configure database_us_2
ok: [eu.db.example.com] => (item=database_eu_2) =>
msg: Configure database_eu_2
The next option is to put the name of the dictionaries to a host-specific variable, e.g.
shell> cat hosts
eu.db.example.com my_db=databases_eu
us.db.example.com my_db=databases_us
Then the playbook below gives the same result
- hosts: all
tasks:
- debug:
msg: "Configure {{ item.name }}"
loop: "{{ lookup('vars', my_db) }}"
loop_control:
label: "{{ item.name }}"
The next option is to create the name of the dictionary dynamically, e.g.
- hosts: all
tasks:
- debug:
msg: "Configure {{ item.name }}"
loop: "{{ lookup('vars', my_db) }}"
loop_control:
label: "{{ item.name }}"
vars:
my_db: "{{ 'databases_' ~ inventory_hostname.split('.').0 }}"
Thanks. I solved by creating a group_vars dir. Thanks for the input.

How to check if element is defined in a list of objects in ansible

I would like to run an ansible Task only if Object element exist in a list.
that's is the Array
local_users:
- name: user1
sshkey: key
sudo: yes
allow_remote_login: yes
- name: user2
sshkey: key
sudo: yes
allow_remote_login: yes
and that my task:
- name: Set up authorized keys
authorized_key:
user: "{{ item.name }}"
state: present
key: "{{ item.sshkey }}"
loop: "{{ local_users }}"
when: local_users.sshkey is defined
but ansible tell me that ssh_key is not defined:
"skip_reason": "Conditional result was False"
any suggestion?
You should be looking for item.sshkey instead of local_users.sshkey while iterating.
Example:
Using your variables, display a message when sshkey is defined:
- debug:
msg: "{{ item.name }}, has key"
loop: "{{ local_users }}"
when: item.sshkey is defined
Similarly, you should have:
- name: Set up authorized keys
authorized_key:
user: "{{ item.name }}"
state: present
key: "{{ item.sshkey }}"
loop: "{{ local_users }}"
when: item.sshkey is defined

ansible - using array of Variables and loop through it

I have to run steps like this around 20-25 times. How can I do with for loop (with_items).
I can have the the parameters URL1, Location1, pkg1.comamd1, $pkg1.command2 are pre defined and I can have them defined in the ansible play book. the Pkg1 value will be passed from the jenkins script
- get_url:
url: "$URL1"
dest: $Location1
when: $Pkg1 != 'NONE'
- Name : run the commands
Shell: sh $pkg1.comamd1; sh $pkg1.command2
when: Pkg1 != 'NONE'
how can I create an array of variables and do it with_items
VarDetails {Pkg1, URL1, Location1, comamd1a, $command1b
Pkg2, URL2, Location2, comamd2a, $command2b
Pkg3, URL3, Location3, comamd3a, $command3b
....................
....................
}
I haven't tested but it must work with below reference example while using list of items.
- name: more complex items to add several users
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: "{{ item.groups }}"
state: present
with_items:
- { name: testuser1, uid: 1002, groups: "wheel, staff" }
- { name: testuser2, uid: 1003, groups: staff }
dont forget to add item before your variables by changing
url: "$URL1"
dest: $Location1
to
url: "item.url"
dest: "item.location"
and while referencing in with_items.. use your variable $URL1, $URL2

Ansible selectattr + map + list returns empty result

I've been reading and searching a lot and cannot overcome this issue. Can you please help ?
With the content below, I need to extract the attribute "ipAddress" and push to a list. Actual data is a dict of objects such as "host01" but for the example I reduced the data to one host.
{
"host01": {
"fqdn": "host01.mydomain",
"interface": {
"Bundle-Ether1001": {
"ipAddress": "10.20.30.41",
"subnetMask": "255.255.255.252"
},
"Bundle-Ether1002": {
"ipAddress": "10.20.30.45",
"subnetMask": "255.255.255.252"
}
},
"timestamp": 1545420334
}
}
My playbook goes as follows:
---
- name : myplaybook
hosts: localhost
vars:
myjson: "{{ lookup('file', 'api.json') | from_json }}"
tasks:
- name: debug
debug:
msg: "{{myjson}}"
- name: debugallip
when: item.value.interface is defined
debug:
msg: "{{ item | selectattr('ipAddress', 'defined') | map('value.ipAddress') | list }}"
with_dict: "{{ myjson }}"
Which returns an empty list ? :(
TASK [debugallip] ******************************************************************************************
ok: [localhost] => (item={'value': {u'interface': {u'Bundle-Ether1001': {u'subnetMask': u'255.255.255.252', u'ipAddress': u'10.20.30.41'}, u'Bundle-Ether1002': {u'subnetMask': u'255.255.255.252', u'ipAddress': u'10.20.30.45'}}, u'timestamp': 1545420334, u'fqdn': u'host01.mydomain'}, 'key': u'host01'}) => {
"msg": []
}
What am I doing wrong ??
The selectattr filter is incorrect, as the ipAddress is buried down in the structure, and not at the top-level of ... well, anything in your particular setup. One can very easily see the shape of item in the task output, so I have no idea why you would assume any other shape, but here we are. Your invocation of map is incorrect, too, since there is no such filter as value.ipAddress, which only would have actually exploded if your selectattr had been correct.
- debug:
msg: '{{ item.value.interface | dict2items | map(attribute="value.ipAddress") | select("defined") }}'
with_dict: '{{ myjson }}'
If you want all the ipAddress values buried in the host.interface structure, you'll need to essentially do another with_dict loop over the host.interface dict, which is what dict2items does. Then, you can save yourself some copy-pasta by going ahead and extracting .ipAddress, with the risk that it is not defined, and then toss out the undefined ones at the end
An option would be to use json_query
- debug:
msg: "{{ item.value.interface | dict2items | json_query('[].value.ipAddress') }}"

Resources