Registering output to complex variable and placing conditionals on sub-components - loops

I'm getting a bit lost with next variable types and am hoping for some direction in a specific task please:
The Goal:
Based on a list of username:publickey values. I'd like to:
ensure the user exists on the target system
if the user does exist then:
- ensure the "/home/$user/.ssh/authorized_keys" file exists with the correct permissions through the path.
The Scene:
I have a variable:
ssh_vars:
auth_keys:
bob: "bobs_public_key_string"
anne: "annes_public_key_string"
anon: "anons_public_key_string
I need to iterate over this variable and for each auth_keys item call a tasklist:
- name: loop through the auth_keys and call ssh_dirs.yml for each
ansible.builtin.include_tasks: "ssh_dirs.yaml"
loop: "{{ ssh_vars.auth_keys }}"
However, I only really want to do this when the auth_key(key) is a user which already exists on the host.
I have been playing with getent, within "ssh_dirs.yaml":
- name: "Ensure the user exists on the target system"
ansible.builtin.getent:
database: passwd
key: "{{ item.name }}"
fail_key: false
register: userlookup
which creates what i think is a list of dictionaries:
ok: [ans-client.local] => {
"userlookup": {
"changed": false,
"msg": "All items completed",
"results": [
{
"ansible_facts": {
"getent_passwd": {
"bob": [
"x",
"1003",
"1003",
"",
"/home/bob",
"/usr/bin/bash"
]
}
},
"ansible_loop_var": "item",
"changed": false,
"failed": false,
"invocation": {
"module_args": {
"database": "passwd",
"fail_key": false,
"key": "bob",
"service": null,
"split": null
}
},
"item": {
"key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDvIZuaBhAIGShw21rkvgqyvNePunbVs6OtOBhYJOY2P anne#ans-server",
"name": "bob"
}
},
{
"ansible_facts": {
"getent_passwd": {
"anne": [
"x",
"1000",
"1000",
"anne",
"/home/anne",
"/bin/bash"
]
}
},
"ansible_loop_var": "item",
"changed": false,
"failed": false,
"invocation": {
"module_args": {
"database": "passwd",
"fail_key": false,
"key": "anne",
"service": null,
"split": null
}
},
"item": {
"key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKr/76O3hLJlcyZuy7EJxf7sC1z9BSHMuxGsFGBibJY3 anne#ans-server",
"name": "anne"
}
},
{
"ansible_facts": {
"getent_passwd": {
"anon": null
}
},
"ansible_loop_var": "item",
"changed": false,
"failed": false,
"invocation": {
"module_args": {
"database": "passwd",
"fail_key": false,
"key": "anon",
"service": null,
"split": null
}
},
"item": {
"key": "SOMEKEY",
"name": "anon"
},
"msg": "One or more supplied key could not be found in the database."
}
],
"skipped": false
}
}
But I can't figure out how to isolate this list to ensure the include_tasks: is not called if the user doesn't exist.
- name: loop through the auth_keys and call ssh_dirs.yml for each
ansible.builtin.include_tasks: "ssh_dirs.yaml"
loop: "{{ ssh_vars.auth_keys }}"
when: userlookup.results.???????
How can I figure out how to reference this nested variable, and how best to isolate a non-missing user?
Something like userlookup.results.msg is not defined might work but it's very loose - is there something better I'm missing?

See registering variables with loop
The global idea is
loop over you var to get the existing/unavailable users
loop over the results of that previous task for you next one. The original loop variable is available in the item key of each result and you can filter as you like.
For your particular case, in a nutshell (untested):
- name: Ensure the user exists on the target system
ansible.builtin.getent:
database: passwd
key: "{{ item.name }}"
register: userlookup
ignore_errors: true
loop: "{{ ssh_vars.auth_keys }}"
- name: Call ssh_dirs.yml for each existing users
ansible.builtin.include_tasks: ssh_dirs.yaml
loop: "{{ userlookup.results | select('success') }}"
loop_control:
loop_var: user_checked
vars:
userloop: "{{ user_checked.item }}"

I think I've solved it, although maybe there's a better thing to look for in the getent response than just the msg?
The logic and variable reference which works:
- name: "Ensure the user exists on the target system"
ansible.builtin.getent:
database: passwd
key: "{{ item.name }}"
fail_key: false
register: userlookup
loop: "{{ ssh_vars.auth_keys }}"
- name: Build a list of usernames which don't exist on the remote host (missing_users)
ansible.builtin.set_fact:
missing_users: "{{ missing_users | default([]) + [usercheck.item.name | string] }}"
loop: "{{ userlookup.results }}"
loop_control:
loop_var: usercheck
when: usercheck.msg is defined
- name: loop through the users and ensure the necessary user folders and files are there
ansible.builtin.include_tasks: "ssh_dirs.yaml"
loop: "{{ ssh_vars.auth_keys }}"
loop_control:
loop_var: userloop
when: userloop.name not in missing_users
Although this is still checking the msg: output mindlessly so only a partial solution

Related

looping through dictionary-formatted Ansible facts

Considering the output of ios_facts module (or any similar gathering fact module), I wrote the following Ansible playbook:
name: checking interface status
gather_facts: no
hosts: iosxe
tasks:
name: get cisco config
cisco.ios.ios_facts:
gather_subset: all
register: cisco_output
name: task001
debug:
var: cisco_output['ansible_facts']['ansible_net_interfaces']
loop: "{{ ansible_facts['ansible_net_interfaces'] | dict2items }}"
when: item.lineprotocol == "up"
For the sake of practicing Ansible, I wanted the playbook to show list of interfaces in "up" state. The relative part in the output of the "ios_fact" shows that it is a "dictionary" not "list".
"ansible_net_interfaces": {
"GigabitEthernet1": {
"bandwidth": 1000000,
"description": null,
"duplex": "Full",
"ipv4": [
{
"address": "10.106.31.229",
"subnet": "24"
}
],
"lineprotocol": "up",
"macaddress": "000c.29c3.a8f3",
"mediatype": "Virtual",
"mtu": 1500,
"operstatus": "up",
"type": "CSR vNIC"
},
"GigabitEthernet2": {
"bandwidth": 1000000,
"description": "CSR2",
"duplex": "Full",
"ipv4": [
{
"address": "10.171.73.33",
"subnet": "27"
}
],
"lineprotocol": "up",
"macaddress": "000c.29c3.a8fd",
"mediatype": "Virtual",
"mtu": 1500,
"operstatus": "up",
"type": "CSR vNIC"
}, ...
I got several different errors and tried to workaround by changing the playbook each time as these:
loop: "{{ ansible_facts['ansible_net_interfaces'] | dict2items }}"
loop: "{{ cisco_output['ansible_facts']['ansible_net_interfaces'] }}"
loop: "{{ cisco_output['ansible_facts']['ansible_net_interfaces'] | dict2items }}"
But was unsuccessful as I got different errors again. What would be the right way of this?
The dict2items filter will convert the dict to an array of items, where the keys will be GigabitEthernet1, GigabitEthernet2, and the values of these keys will sub-dicts.
"item.key": "GigabitEthernet1"
"item.key": "GigabitEthernet2"
So, the lineprotocol key will be in the item.value, i.e. item.value.lineprotocol. A simple task such as below can demonstrate the same:
- debug:
msg: "{{ item.key }} is up"
loop: "{{ cisco_output['ansible_facts']['ansible_net_interfaces | dict2items }}"
when: item.value.lineprotocol == "up"
The attribute ipv4 is a list. Use with_subelements if you want to take into account the fact that there might be more IP addresses configured, e.g.
- debug:
msg: "{{ item.0.key }} {{ item.1.address }}"
with_subelements:
- "{{ ansible_net_interfaces|dict2items|
selectattr('value.lineprotocol', 'eq', 'up') }}"
- value.ipv4
gives
msg: GigabitEthernet1 10.106.31.229
msg: GigabitEthernet2 10.171.73.33

How to loop through set_fact attributes in another task of ansible playbook

I am struggling with the looping using with_items from the set_fact results in my ansible playbook.
- set_fact:
TAGNAME: "TEST-EC2-0{{item}}"
TAGOWNER: LOGIN
TAGROLE: DB
with_sequence: start=01 end="{{ count }}"
register: tagname
Below is the result of set_fact using debug I got this.
"results": [
{
"_ansible_ignore_errors": null,
"_ansible_item_label": "1",
"_ansible_item_result": true,
"_ansible_no_log": false,
"ansible_facts": {
"TAGNAME": "TEST-EC2-01",
"TAGOWNER": "LOGIN",
"TAGROLE": "DB"
},
"changed": false,
"failed": false,
"item": "1"
},
{
"_ansible_ignore_errors": null,
"_ansible_item_label": "2",
"_ansible_item_result": true,
"_ansible_no_log": false,
"ansible_facts": {
"TAGNAME": "TEST-EC2-02",
"TAGOWNER": "LOGIN",
"TAGROLE": "DB"
},
"changed": false,
"failed": false,
"item": "2"
}
]
With this given output, I want to use all the "TAGNAME" in the below ec2 task for tagging purpose whenever I create multiple instances using COUNT module.
ec2:
....
....
....
instance_tags:
Name: "{{ item.TAGNAME }}"
Owner: "LOGIN"
with_items: "{{ tagname.results }}"
But when I trigger the playbook I am getting below error
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'item' is undefined\n\nThe error appears to have been in 'local/apps/roles/EC2_Deploy/tasks/ec2_creation.yml': line 217, 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: Create Instances\n ^ here\n"}
I am not sure what and where I am missing. Could someone help me on this ?

Loop isn't iterating over last line of list

I have created a template within a playbook which I want to iterate through with a list of hashes. The output of this I want to add to another var to use in a following module.
The template works and the loop looks like it works, but it never adds the last item in the list. I have recreated it in a test play.
---
- hosts: localhost
tasks:
- name: Init
set_fact:
foo: []
fqdn: "test.com"
template: []
- name: portlist
set_fact:
portlist:
- { port: 9091, index: 1 }
- { port: 9092, index: 2 }
- { port: 9093, index: 3 }
- { port: 9094, index: 4 }
- name: generate policy
set_fact:
template:
- name: "traffic to {{ item.port }}"
index: "{{ item.index }}"
match:
desc: "[{{ item.port }}]" # The field needs to be passed as a list
name: "{{ fqdn }}_{{ item.port }}"
port: "{{ item.port }}"
foo: "{{ foo + template }}"
loop: "{{ portlist }}"
- debug:
var: foo
I understand I can make this play smaller with defaults rather than initializing vars but this felt easier to read for troubleshooting.
The play results in a list of hashes which then I can input into a policy module. However it only ever gives me 3 items in the list and misses off the last item in the portlist.
TASK [debug] ***************************************************
ok: [localhost] => {
"foo": [
{
"action": {
"ref": "test.com_9091",
"this": true
},
"enable": true,
"index": "1",
"match": {
"port": {
"criteria": "IS_IN",
"port": [
9091
]
}
},
"name": "traffic to 9091"
},
{
"action": {
"ref": "test.com_9092",
"this": true
},
"enable": true,
"index": "2",
"match": {
"port": {
"criteria": "IS_IN",
"port": [
9092
]
}
},
"name": "traffic to 9092"
},
{
"action": {
"ref": "test.com_9093",
"this": true
},
"enable": true,
"index": "3",
"match": {
"port": {
"criteria": "IS_IN",
"port": [
9093
]
}
},
"name": "traffic to 9093"
}
]
}
Your problem is caused by the fact that variables defined via set_fact aren't available until after the set_fact task has finished. This means that when you set:
foo: "{{ foo + template }}"
You see the value of template from the previous loop iteration.
One way of dealing with this is to rewrite your set_fact task to set foo directly:
---
- hosts: localhost
gather_facts: false
vars:
fqdn: "test.com"
portlist:
- {port: 9091, index: 1}
- {port: 9092, index: 2}
- {port: 9093, index: 3}
- {port: 9094, index: 4}
tasks:
- name: generate policy
set_fact:
foo: >-
{{
foo + [{
'name': 'traffic to {}'.format(item.port),
'index': item.index,
'match': {
'desc': "[{}]".format(item.port),
'name': '{}_{}'.format(fqdn, item.port),
'port': item.port
}
}]
}}
vars:
foo: []
loop: "{{ portlist }}"
- debug:
var: foo
This will output:
TASK [debug] *********************************************************************************************************************************************************************************
ok: [localhost] => {
"foo": [
{
"index": 1,
"match": {
"desc": "[9091]",
"name": "test.com_9091",
"port": 9091
},
"name": "traffic to 9091"
},
{
"index": 2,
"match": {
"desc": "[9092]",
"name": "test.com_9092",
"port": 9092
},
"name": "traffic to 9092"
},
{
"index": 3,
"match": {
"desc": "[9093]",
"name": "test.com_9093",
"port": 9093
},
"name": "traffic to 9093"
},
{
"index": 4,
"match": {
"desc": "[9094]",
"name": "test.com_9094",
"port": 9094
},
"name": "traffic to 9094"
}
]
}
If you find your template-based solution more readable, you could rewrite it using two set_fact tasks like this:
---
- hosts: localhost
gather_facts: false
vars:
fqdn: "test.com"
portlist:
- {port: 9091, index: 1}
- {port: 9092, index: 2}
- {port: 9093, index: 3}
- {port: 9094, index: 4}
tasks:
- name: generate policy
set_fact:
template:
name: "traffic to {{ item.port }}"
index: "{{ item.index }}"
match:
desc: "[{{ item.port }}]" # The field needs to be passed as a list
name: "{{ fqdn }}_{{ item.port }}"
port: "{{ item.port }}"
loop: "{{ portlist }}"
register: foo
- set_fact:
foo: "{{ foo.results | map(attribute='ansible_facts.template') | list }}"
- debug:
var: foo

How to access register variable in task

This is my output of EC2 instance. I am trying to access "instance_type".
And here is my task.
ec2:
key_name: redhat
group: MY_EC2
instance_type: t2.micro
image: ami-cfe4b2b0
region: us-east-1
zone: us-east-1a
wait: true
exact_count: 1
count_tag:
name: MyProjectInstances
instance_tags:
name: Ansible
register: ec2
- set_fact:
inst: "{{ ec2 }}"
- debug:
msg: "{{ inst }}"
I can reach Instances block through trying this.
debug:
msg: "{{ inst.instances }}" but cannot go further, getting error of undefined variable.
#
Output:
ok: [localhost] => {
"msg": {
"changed": true,
"failed": false,
"instance_ids": [
"i-0be089202b191769e"
],
"instances": [
{
"ami_launch_index": "0",
"architecture": "x86_64",
"block_device_mapping": {
"/dev/xvda": {
"delete_on_termination": true,
"status": "attached",
"volume_id": "vol-02b129004f1a5fb89"
}
},
"dns_name": "ec2-34-204-84-170.compute-1.amazonaws.com",
"ebs_optimized": false,
"groups": {
"sg-06c09a2c83d7b1a96": "MY_EC2"
},
"hypervisor": "xen",
"id": "i-0be089202b191769e",
"image_id": "ami-cfe4b2b0",
"instance_type": "t2.micro",
"kernel": null,
"key_name": "redhat",
"launch_time": "2018-07-15T14:34:43.000Z",
"placement": "us-east-1a",
"private_dns_name": "ip-172-31-35-24.ec2.internal",
"private_ip": "172.31.35.24",
"public_dns_name": "ec2-34-204-84-170.compute-1.amazonaws.com",
"public_ip": "34.204.84.170",
"ramdisk": null,
"region": "us-east-1",
"root_device_name": "/dev/xvda",
"root_device_type": "ebs",
"state": "running",
"state_code": 16,
"tags": {
"name": "Ansible"
},
"tenancy": "default",
"virtualization_type": "hvm"
}
Please try as below::
- name: Get instance Type
debug: msg={{ inst | json_query('instances[].instance_type') }}

ansible: how to iterate over all registered results?

Given the following playbook:
---
- name: Check if log directory exists - Step 1
stat: path="{{ wl_base }}/{{ item.0.name }}/{{ wl_dom }}/servers/{{ item.1 }}/logs" get_md5=no
register: log_dir
with_subelements:
- wl_instances
- servers
- name: Check if log directory exists - Step 2
fail: msg="Log directory does not exists or it is not a symlink."
failed_when: >
log_dir.results[0].stat.islnk is not defined
or log_dir.results[0].stat.islnk != true
or log_dir.results[0].stat.lnk_source != "{{ wl_base }}/logs/{{ wl_dom }}/{{ item.1 }}"
with_subelements:
- wl_instances
- servers
that is using the following vars:
---
wl_instances:
- name: aservers
servers:
- AdminServer
- name: mservers
servers:
- "{{ ansible_hostname }}"
the second task currently only uses one of the two possible results (results[0]).
My question is: how could I iterate over all available items stored in log_dir.results?
A sample output debug:hostvars[inventory_hostname] follows:
"log_dir": {
"changed": false,
"msg": "All items completed",
"results": [
{
"changed": false,
"invocation": {
"module_args": "path=\"/path/to/servers/aservers/domain/AdminServer/logs\" get_md5=no",
"module_name": "stat"
},
"item": [
{
"name": "aservers"
},
"AdminServer"
],
"stat": {
...
"lnk_source": "/path/to/logs/domain/AdminServer",
...
}
},
{
"changed": false,
"invocation": {
"module_args": "path=\"/path/to/servers/mservers/domain/servers/some_hostname/logs\" get_md5=no",
"module_name": "stat"
},
"item": [
{
"name": "mservers"
},
"some_hostname"
],
"stat": {
...
"lnk_source": "/path/to/logs/domain/some_hostname",
...
Looping over the results in an array (denoted by the []), would be done as
with_items: somelist
or if it's a dict that contains a list, as in this case
with_items: log_dir.results
note this can also be written
with_items: log_dir['results']
so in your task
- name: Check if log directory exists - Step 2
fail: msg="Log directory does not exists or it is not a symlink."
failed_when: >
item.stat.islnk is not defined
or item.stat.islnk != true
or item..stat.lnk_source != "{{ wl_base }}/logs/{{ wl_dom }}/{{ item.1 }}"
with_items: log_dir.results
More information and examples is available in http://docs.ansible.com/playbooks_loops.html#standard-loops.
The main thing here is that you're wanting to access only part of the registered variable.
My debug output:
{
"dkim_key.results": [
{
"changed": false,
"invocation": {
"module_args": "path=/etc/opendkim/keys/accept.example.com/mail.private get_md5=no",
"module_name": "stat"
},
"item": "accept.example.com",
"stat": {
"atime": 1427461574.5667424,
"checksum": "c882abaabvc66257555929f6290480a409d1",
"ctime": 1427461575.0307424,
"dev": 64770,
"exists": true,
"gid": 119,
"inode": 521115,
"isblk": false,
"ischr": false,
"isdir": false,
"isfifo": false,
"isgid": false,
"islnk": false,
"isreg": true,
"issock": false,
"isuid": false,
"mode": "0600",
"mtime": 1427461574.5947425,
"nlink": 1,
"pw_name": "opendkim",
"rgrp": false,
"roth": false,
"rusr": true,
"size": 887,
"uid": 110,
"wgrp": false,
"woth": false,
"wusr": true,
"xgrp": false,
"xoth": false,
"xusr": false
}
},
{
"changed": false,
"invocation": {
"module_args": "path=/etc/opendkim/keys/test.example.com/mail.private get_md5=no",
"module_name": "stat"
},
"item": "test.example.com",
"stat": {
"exists": false
}
}
]
}
Found the solution for a similar problem as follows:
- name: DKIM | Generate signing key
shell: opendkim-genkey -s {{ postfix.dkim_selector }} -d {{ item.item }} -D /etc/opendkim/keys/{{ item.item }}
with_items: dkim_key.results
when: not item.stat.exists
notify: restart opendkim
tags:
- postfix
- dkim
Using the dkim_key.results and a list to iterate over and then check against that list with item.stat.exists. Lastly getting the actual item via item.item

Resources