Create var based on list in dict - ansible

Imagine this dict on 4 different hosts.
# on host 1
my_dict:
ip: 10.0.0.111
roles:
- name: something
observer: false
# on host 2
my_dict:
ip: 10.0.0.112
roles:
- name: something
observer: false
# on host 3
my_dict:
ip: 10.0.0.113
roles:
- name: something
observer: true
# on host 4
my_dict:
ip: 10.0.0.114
roles:
- name: whatever
When Ansible runs for all 4 hosts I want it to build a variable for each host having the roles name 'something'. The desired output is:
10.0.0.111 10.0.0.112 10.0.0.113:observer
There are 2 requirements:
when my_dict.roles.name == 'something' it must add the ip to the var
but when my_dict.roles.observer , it must add the ip + ':observer'
I eventually want to use the var in a Jinja template, so to me, the var can be either set via an Ansible task or as a jinja template.
This doesn't work:
- name: set fact for ip
debug:
msg: >-
{{ ansible_play_hosts |
map('extract', hostvars, ['my_dict', 'ip'] ) |
join(' ') }}
when: ???

You could create two lists:
one with what should be postfixed to the IPs with the condition based on the observer property
the other one with the IPs
And then zip them back together.
Given:
- debug:
msg: >-
{{
_ips | zip(_is_observer) | map('join') | join(' ')
}}
vars:
_hosts: >-
{{
hostvars
| dict2items
| selectattr('key', 'in', ansible_play_hosts)
| selectattr('value.my_dict.roles.0.name', '==', 'something')
}}
_is_observer: >-
{{
_hosts
| map(attribute='value.my_dict.roles.0.observer')
| map('replace', false, '')
| map('replace', true, ':observer')
}}
_ips: >-
{{
_hosts
| map(attribute='value.my_dict.ip')
}}
This yields:
TASK [debug] *************************************************************
ok: [localhost] =>
msg: 10.0.0.111 10.0.0.112 10.0.0.113:observer

Related

Ansible playbook loop from site yaml or template?

I'm trying to use my Ansible playbook to call upon a site YAML reference to create a filename that increment for multiple switches. What am I doing wrong? I believe the playbook is pulling from the host YAML?
Format: <switch>-<site>-<floor><stackid>.txt
e.g.: with two switches:
swi-lon-101.txt
swi-lon-202.txt
host_vars/host.yaml
project_name: test
device_name: swi
site_abbrev: lon
device_type: switch
switch_stacks:
- id: 01
installation_floor: 1
- id: 02
installation_floor: 2
templates/switch-template.j2
{% for stack in switch_stacks %}
set system host-name {{ device_name }}-{{ site_abbrev }}-{{ stack.installation_floor }}{{ stack.id }}
{% endfor %}
The playbook, in which the problem lies, how do I get the hostname to create correctly for each of the 2 switches?
My playbook:
- name: Create Folder Structure
hosts: junos
gather_facts: false
tasks:
- name: Create Site Specific Folder
file:
path: /home/usr/complete_config/{{ project_name }}
state: directory
mode: 0755
- name: Set Destination Directory & Filename for Switch Configurations
set_fact:
dest_dir: /home/usr/complete_config/{{ project_name }}
full_device_name: "{{ device_name|lower }}-{{ site_abbrev|lower }}-{{ switch_stacks.installation_floor }}{{ switch_stacks.id }}.txt"
when: device_type == 'switch'
Ansible error, running:
ansible-playbook playbooks/site-playbook.yaml
TASK [Set Destination Directory & Filename for Switch Configurations] **************************************************
fatal: [site-switch]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'list object' has no attribute 'installation_floor'\n\nThe error appears to be in '/home/usr/playbooks/switch-playbook.yaml': line 19, column 7, 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 Destination Directory & Filename for Switch Configurations\n ^ here\n"}
So, you do need a loop in order to set this fact, otherwise, you are trying to access a installation_floor on a list, which cannot be.
You will also face an issue with the id of your items in switch_stacks, as 01 is an int and will end up displayed as 1, simply. So you either need to declare those as string, or to pad them with a format filter.
So, you end up with this task:
- set_fact:
full_device_name: >-
{{
full_device_name
| default([])
+ [
device_name | lower ~ '-' ~
site_abbrev | lower ~ '-' ~
item.installation_floor ~
"%02d" | format(item.id) ~ '.txt'
]
}}
loop: "{{ switch_stacks }}"
when: device_type == 'switch'
Which will create a list:
full_device_name:
- swi-lon-101.txt
- swi-lon-202.txt
Given the playbook:
- hosts: localhost
gather_facts: false
tasks:
- set_fact:
full_device_name: >-
{{
full_device_name
| default([])
+ [
device_name | lower ~ '-' ~
site_abbrev | lower ~ '-' ~
item.installation_floor ~
"%02d" | format(item.id) ~ '.txt'
]
}}
loop: "{{ switch_stacks }}"
when: device_type == 'switch'
vars:
device_name: swi
site_abbrev: lon
device_type: switch
switch_stacks:
- id: 01
installation_floor: 1
- id: 02
installation_floor: 2
- debug:
var: full_device_name
This yields:
TASK [set_fact] ************************************************************
ok: [localhost] => (item={'id': 1, 'installation_floor': 1})
ok: [localhost] => (item={'id': 2, 'installation_floor': 2})
TASK [debug] ***************************************************************
ok: [localhost] =>
full_device_name:
- swi-lon-101.txt
- swi-lon-202.txt
Let's keep the attribute id as a string
switch_stacks:
- id: '01'
installation_floor: 1
- id: '02'
installation_floor: 2
The task
- set_fact:
full_device_name: "{{ [prefix]|product(sw_fl_id)|map('join')|
product([postfix])|map('join')|list }}"
vars:
prefix: "{{ device_name }}-{{ site_abbrev }}-"
postfix: ".txt"
sw_id: "{{ switch_stacks|map(attribute='id')|list }}"
sw_fl: "{{ switch_stacks|map(attribute='installation_floor')|list }}"
sw_fl_id: "{{ sw_fl|zip(sw_id)|map('join')|list }}"
gives you the list
full_device_name:
- swi-lon-101.txt
- swi-lon-202.txt
#β.εηοιτ.βε
Thanks for your reply, it worked.
I have however slightly tweeked it as you will see below.
Once again thanks for showing the way
name: Set Destination Directory & Filename for Switch Configurations
set_fact:
dest_dir: /home/usr/complete_config/{{ project_name }}
full_device_name: >-
{{
device_name | lower ~ '-' ~
site_abbrev | lower ~ '-' ~
item.installation_floor ~
"%02d" | format(item.id)
}}
loop: "{{ switch_stacks }}"
when: device_type == 'switch'

search a host in ansible inventory

I have below ansible inventory structure:
[app1]
labhost1 ansible_host=1.1.1.1 tag=master
labhost2 ansible_host=1.1.1.2 tag=slave
labhost3 ansible_host=1.1.1.3 tag=slave
labhost4 ansible_host=1.1.1.4 tag=master
labhost5 ansible_host=1.1.1.5 tag=slave
labhost6 ansible_host=1.1.1.6 tag=slave
[DC1]
dc1_app1
[DC2]
dc2_app1
[dc1_app1]
labhost1 ansible_host=1.1.1.1 tag=master
labhost2 ansible_host=1.1.1.2 tag=slave
labhost3 ansible_host=1.1.1.3 tag=slave
[dc2_app1]
labhost4 ansible_host=1.1.1.4 tag=master
labhost5 ansible_host=1.1.1.5 tag=slave
labhost6 ansible_host=1.1.1.6 tag=slave
and group_vars file below:
DC1.yml
---
location: country1
DC2.yml
---
location: country2
When running a playbook on labhost2, I like to extract the IP address of the master device in the same datacenter in which the host labhost2 is located.
I tried below expression
- set_fact:
masterIP: "{{ groups['app1'] | map('extract', hostvars) | selectattr('location', 'eq', location) | selectattr('tag', 'eq', 'master') | map(attribute='ansible_host') }}"
It should return 1.1.1.1 as value of the variable masterIP but it shows:
VARIABLE IS UNDEFINED
From the logic of the question, I can only assume that you run the playbook for the group app1
- hosts: app1
Iterate the 'dc*' groups that belong to the group of applications and create the dictionary of masters, e.g.
- set_fact:
dc_masters: "{{ dc_masters|d({})|combine({item: _dict.master}) }}"
loop: "{{ groups|select('match', 'dc[\\d+]_app1') }}"
vars:
_hosts: "{{ groups[item] }}"
_tags: "{{ _hosts|map('extract', hostvars, 'tag') }}"
_dict: "{{ dict(_tags|zip(_hosts)) }}"
run_once: true
gives
dc_masters:
dc1_app1: labhost1
dc2_app1: labhost4
Then you can use this dictionary and find the masters for the slaves, e.g.
- debug:
msg: "slave: {{ inventory_hostname }} master: {{ dc_masters[dc] }}"
vars:
dc: "{{ group_names|difference(['app1'])|first }}"
when: tag == 'slave'
gives
TASK [debug] **************************************************************
skipping: [labhost1]
skipping: [labhost4]
ok: [labhost2] =>
msg: 'slave: labhost2 master: labhost1'
ok: [labhost3] =>
msg: 'slave: labhost3 master: labhost1'
ok: [labhost6] =>
msg: 'slave: labhost6 master: labhost4'
ok: [labhost5] =>
msg: 'slave: labhost5 master: labhost4'
Fit the expression dc: ... to your needs if there are more groups a slave belongs to.

Ansible regex replace in variable to cisco interface names

I'm currently working with a script to create interface descriptions based on CDP neighbor info, but it's placing the full names e.g., GigabitEthernet1/1/1, HundredGigabitEthernet1/1/1.
My regex is weak, but I would like to do a regex replace to keep only the first 3 chars of the interface name.
I think a pattern like (dredGigatbitEthernet|abitEthernet|ntyGigabitEthernet|etc) should work, but not sure how to put that into the playbook line below to modify the port value
nxos_config:
lines:
- description {{ item.value[0].port }} ON {{ item.value[0].host }}
E.g, I am looking for GigabitEthernet1/1/1 to end up as Gig1/1/1
Here is an example of input data:
{
"FastEthernet1/1/1": [{
"host": "hostname",
"port": "Port 1"
}]
}
Final play to make it correct using ansible net neighbors as the source
Thank you - I updated my play, adjusted for ansible net neighbors
- name: Set Interface description based on CDP/LLDP discovery
hosts: all
gather_facts: yes
connection: network_cli
tasks:
- debug:
msg: "{{ ansible_facts.net_neighbors }}"
- debug:
msg: >-
description
{{
item[0].port
| regex_search('(.{3}).*([0-9]+\/[0-9]+\/[0-9]+)', '\1', '\2')
| join
}}
ON {{ item.value[0].host }}"
loop: "{{ ansible_facts.net_neighbors | dict2items }}"
loop_control:
label: "{{ item.key }}"
Thanks for the input!
Given that you want the three first characters along with the last 3 digits separated by a slash, then the regex (.{3}).*([0-9]+\/[0-9]+\/[0-9]+) should give you two capturing groups containing those two requirements.
In Ansible, you can use regex_search to extract those groups, then join them back, with the help on the join Jinja filter.
Given the playbook:
- hosts: localhost
gather_facts: no
tasks:
- debug:
msg: >-
description
{{
item.key
| regex_search('(.{3}).*([0-9]+\/[0-9]+\/[0-9]+)', '\1', '\2')
| join
}}
ON {{ item.value[0].host }}"
loop: "{{ interfaces | dict2items }}"
loop_control:
label: "{{ item.key }}"
vars:
interfaces:
GigabitEthernet1/1/1:
- port: Port 1
host: example.org
HundredGigabitEthernet1/1/1:
- port: Port 2
host: example.com
This yields:
TASK [debug] ***************************************************************
ok: [localhost] => (item=eth0) =>
msg: description Gig1/1/1 ON example.org"
ok: [localhost] => (item=eth1) =>
msg: description Hun1/1/1 ON example.com"

Ansible returns only one item from a dictionary

I'm trying to get a list of IPs from an specific Service, and ansible returns only one item from the loop.
I have tried many things and is always the same result.
Need help.
- name: "Amazon IPs"
include_vars:
file: /home/user1/ansible/AWS/ip-ranges.json
name: amazon
# - set_fact:
# # # test: "{{ (variable.stdout | from_json).prefixes | map(attribute='ip_prefix') | list }}"
# amazonipv4: "{{ item }}"
# # amazonipv6: "{{ amazon.ipv6_prefixes | map(attribute='ipv6_prefix') | list }}"
# loop: "{{amazon.prefixes | map(attribute='ip_prefix') | list }}"
# # when: '"AMAZON" in item.service'
- set_fact:
test3: "{{item.ip_prefix}}"
loop: "{{amazon.prefixes | list }}"
when: '"AMAZON" in item.service'
- debug:
var: test3
I expect to get a list based on the service, but I only get one item.
example:
TASK [debug] ***********************************************
ok: [localhost] => {
"test3": "54.190.198.32/28"
in each iteration in the set_fact loop, you are setting the value, not pushing to a list. you need to change your syntax to:
- set_fact:
test3: "{{ test3 | default([]) + [item.ip_prefix] }}"
hope it helps.

Ansible get first element from list

Suppose I have the following vars_file:
mappings:
- primary: 1.1.1.1
secondary: 2.2.2.2
- primary: 12.12.12.12
secondary: 11.11.11.11
and hosts file
1.1.1.1
12.12.12.12
5.5.5.5
and the following playbook task
- name: Extract secondary from list
debug:
msg: "{{ (mappings | selectattr('primary', 'search', inventory_hostname) | list | first | default({'secondary':None})).secondary }}"
The current task works and will give empty string when no match are found, but I would like to know if there is a better way/cleaner way of doing it without passing a dictionary to the default constructor.
An option would be to use json_query
- debug:
msg: "{{ mappings | json_query(\"[?primary=='\" + inventory_hostname + \"'].secondary\") }}"
, but selectattr works too
- debug:
msg: "{{ mappings | selectattr('primary', 'equalto', inventory_hostname) | map(attribute='secondary') | list }}"

Resources