I want to define a dictionary variable that various host groups can add their own keys to in group_vars (not using set_fact). E.g. something like this:
group_vars\ftp_servers.yml:
important_ports:
ftp: 21
group_vars\web_servers.yml:
important_ports:
http: 80
so that when run on a server with both of these roles the dictionary is combined, i.e. important_ports =
{
ftp: 21,
http: 80
}
This is exactly what hash_behaviour = merge does, but it's deprecated and will be removed in Ansible 2.13. How do I achieve the same thing without that?
The only solution I've seen recommended is to use the combine filter:
set_fact:
important_ports: "{{ important_ports | combine({ http: 80 }) }}"
This works in a set_fact task, but fails in group_vars with "recursive loop detected in template string: {{ important_ports | combine({ http: 80 }) }}"
I even tried initialising the variable to empty dictionary (important_ports: {}) in group_vars/all, which is supposed to be evaluated before other group_vars, but it still gives the same error.
A (not so clean...) possible solution to this specific problem. This is based on convention where you use the group name in the var name for specific group ports.
Note that if you redefine the same port name with a different value, the latest loaded group will win.
Given the following all-in-one inventory placed in inventories/mergegroups/hosts.yml
---
all:
vars:
ansible_connection: local
important_ports: >-
{%- set result={} -%}
{%- for group in groups -%}
{{ result.update(lookup('vars', group + '_important_ports', default={})) }}
{%- endfor -%}
{{ result }}
ftp_servers:
vars:
ftp_servers_important_ports:
ftp: 21
hosts:
a:
b:
web_servers:
vars:
web_servers_important_ports:
http: 80
hosts:
a:
other_group:
vars:
other_group_important_ports:
mysql: 3306
hosts:
a:
b:
no_port_group:
# as you can see no port definition here.
hosts:
a:
b:
c:
We get the following results for the 3 different host:
$ ansible -i inventories/mergegroups/ a -m debug -a msg="{{ important_ports }}"
a | SUCCESS => {
"msg": {
"ftp": 21,
"http": 80,
"mysql": 3306
}
}
$ ansible -i inventories/mergegroups/ b -m debug -a msg="{{ important_ports }}"
b | SUCCESS => {
"msg": {
"ftp": 21,
"mysql": 3306
}
}
$ ansible -i inventories/mergegroups/ c -m debug -a msg="{{ important_ports }}"
c | SUCCESS => {
"msg": {}
}
Related
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
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"
I have an Excel spreadsheet that I've saved as a CSV file (comma deliminated) containing the IP addresses of multiple interfaces for a list of servers. There's been an interface configured on these servers (initially) so I have connectivity. I'd like to go through this file row-by-row and grab the necessary values to build the ifcfg file locally, copy to the server and then restart the network.
This is not the actual file; rather, a sample outlining the idea that there's a location with multiple IP addresses provided.
New Orleans, 192.168.10.42, 13, 192.168.3.10
Atlanta, 192.168.31.100, 18, 192.168.10.10
Detroit, 172.16.31.8, 43, 172.16.10.27
The goal is to parse this file and create the network interface files as follows
ifcfg-eth0
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=none
NM_CONTROLLED=no
IPADDR=192.168.10
ifcfg-ens3
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=none
NM_CONTROLLED=no
IPADDR=192.168.3.10
This looks to get me partway there
- debug:
loop: "{{ lookup('file', './file.one').splitlines() }}"
register: val
And returns
ok: [localhost] => (item=New Orleans, 192.168.10.42, 13, 192.168.3.10) => {
"msg": "Hello world!"
}
ok: [localhost] => (item=Atlanta, 192.168.31.100, 18, 192.168.10.10) => {
"msg": "Hello world!"
}
ok: [localhost] => (item=Detroit, 172.16.31.8, 43, 172.16.10.27) => {
"msg": "Hello world!"
}
I did find a snippet on StackOverflow using the_dict I used but everything is returned as a list and not a scalar :(
tasks:
- debug: var=the_dict
vars:
the_dict: >-
{%- set row = lookup("file", "~/Documents/Book1.csv").split("\n") | list -%}
{%- for i in row -%}
{%- set v = i.split(",") -%}
{%- set A_Location = v.0 -%}
{%- set B_Interface = v.1 -%}
...
...
...
to read csv with ansible, read module CSV
an example to use template
the template file conf.j2 (to put in templates directory)
{% for record in records.list %}
ifcfg-eth0
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=none
NM_CONTROLLED=no
IPADDR={{ record.ip1 }}
ifcfg-ens3
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=none
NM_CONTROLLED=no
IPADDR=1{{ record.ip2 }}
{% endfor %}
- name: playbook2.0
hosts: localhost
gather_facts: False
tasks:
- name: Read CSV file and return a list
community.general.read_csv:
path: file.csv
fieldnames: location,ip1,data,ip2
delimiter: ','
register: records
delegate_to: localhost
- name: generate fileconf.txt from conf.j2
template:
src: conf.j2
dest: fileconf.txt
you have the new file conf generated in the file fileconf.txt
you could adapt the file.j2 as you want with jinja2 syntax
I am trying to parse the output of elasticache_facts ansible module, to extract the IPS and the ports of the memcached nodes in a string of the form "addr1: port1 addr2:port2 ..."(I want to store this string in a configmap to be used in an app).
Basically I want to take two fields "address" and "port" from a list of dicts like this:
list1:
- endpoint:
address: "addr1"
port: "port1"
- endpoint:
address: "addr2"
port: "port2"
and to concatenate them like above.
I have a ugly solution that goes like this:
# register the output of the facts to something I know
elasticache_facts:
region: "{{ terraform.region }}"
name: "{{ cluster }}-{{ env }}"
register: elasticache
#declare an empty var in vars file to be used as accumulator
memcache_hosts: ""
# iterate through the list of nodes and append the fields to my string; I will have some extra spaces(separators) but that's ok
set_fact:
memcache_hosts: "{{ memcache_hosts }} {{item.endpoint.address}}:{{item.endpoint.port}}"
with_items: "{{ elasticache.elasticache_clusters[0].cache_nodes}}"
Is there some less ugly way to filter the list to the desired format?
Maybe there is a magic filter I don't know about?
I can also obtain two lists, one with hosts, one with ports, zip them, make a dict out of that, but I found only some ugly to_json and then regex to make it a string.
I am also considering to write a custom filter in python, but seems also overdoing it.
Thanks for the help!
Here are two ways to achieve what you are looking for:
#!/usr/bin/env ansible-playbook
---
- name: Lets munge some data
hosts: localhost
become: false
gather_facts: false
vars:
my_list:
- address: '10.0.0.0'
port: '80'
- address: '10.0.0.1'
port: '88'
tasks:
- name: Quicky and dirty inline jinja2
debug:
msg: "{% for item in my_list %}{{ item.address }}:{{ item.port }}{% if not loop.last %} {% endif %}{% endfor %}"
# Note the to_json | from_json workaround for https://github.com/ansible/ansible/issues/27299
- name: Using JSON Query
vars:
jmes_path: "join(':', [address, port])"
debug:
msg: "{{ my_list | to_json | from_json | map('json_query', jmes_path) | join(' ') }}"
The above outputs:
PLAY [Lets munge some data] **********************************************************************************************************************************
TASK [Quicky and dirty inline jinja2] ************************************************************************************************************************
ok: [localhost] => {
"msg": "10.0.0.0:80 10.0.0.1:88"
}
TASK [Using JSON Query] **************************************************************************************************************************************
ok: [localhost] => {
"msg": "10.0.0.0:80 10.0.0.1:88"
}
PLAY RECAP ***************************************************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0
In Puppet I can extract the number of the Hostname with this example:
$host_number = regsubst($hostname, '^\w+(\d\d)', '\1')
Is there something similar in Ansible?
e.g.:
fqdn: test01.whatever
hostname: test01
output -> newvariable: 01
I want to extract only the number out of the Hostname, so I can use it in my Playbook as a variable.
This will fetch the inventory_hostname and replace any character text with nothing, leaving the numeric text. Obviously, you can use your imagination to apply whatever regex needed.
Ansible playbook:
vars:
host_names:
- {{ inventory_hostname | regex_replace('[^0-9]','') }}
Jinja2 Template:
{% for host in groups['some_group_in_inventory'] %}
Some description of the host where I needed the Number {{ host | regex_replace('[^0-9]','') }}
{% endfor %}
Theres multiple ways of doing the same thing...
You can use ansible_facts, ansible_hostname, shell commands, etc...
---
- hosts: localhost
gather_facts: yes
tasks:
- debug: var=ansible_facts.hostname
- debug: var=ansible_hostname
As raVan96 said, you need to explain better what you need and what you expect...
If you need to extract only "test01" from hostname, then you can use ansible build in filter "regex_replace". Here is my example:
{{ ansible_hostname | regex_replace('^([a-zA-Z_]+)(\d+)$','\\2') | int }}
Then you get: 1
If you need "01", then delete part to pass to integer - "int"