Create dynamic variables in Jinja2 - jinja2

I'm trying to create router configs automatically with Ansible and a Jinja2 template. My vars are in two different files: one valid for all routers (all.yml) and an additional one per router (router.yml). I'd like to create a variable to look up the IP address in the router.yml but it seems I can get it right:
all.yml:
vlans:
- { id: 2001, name: TRUSTED-NET }
- { id: 2002, name: UNTRUSTED-NET }
router.yml:
2001_ip: 10.200.2.1 255.255.255.0
2002_ip: 10.168.28.1 255.255.255.0
makeconfig.j2
{% for vlan in vlans %}
interface Vlan{{ vlan.id }}
description {{ vlan.name }}
ip address {{ vlan.ip+'_ip' }} -> TypeError: unsupported operand type(s) for +: 'int' and 'str'
ip address {{ vlan.id|string+'_ip' }} -> output is: ip address 2001_ip
How do I have to specify the variable correctly in order to replace it with the value from router.yml?

You can use the classic dictionary key access in Jinja as well, e.g. router[vlan.id ~ '_ip']. The tilde operator is for string concatenation.
{% set vlans = [{'id': 2001, 'name': 'LAN 1'},
{'id': 2002, 'name': 'LAN 1'}] %}
{% set router = {'2001_ip': '10.200.2.1 255.255.255.0',
'2002_ip': '10.168.28.1 255.255.255.0'} %}
{% for vlan in vlans %}
interface Vlan{{ vlan.id }}
description {{ vlan.name }}
ip address {{ router[vlan.id ~ '_ip'] }}
{% endfor %}
Output:
interface Vlan2001
description LAN 1
ip address 10.200.2.1 255.255.255.0
interface Vlan2002
description LAN 1
ip address 10.168.28.1 255.255.255.0

Related

SaltStack check if grain exists in Jinja file

I'm using SaltStack to manage my infra. Machines are hosted in different DCs, so they also have slightly different network setup.
Currently, I'm running into the following issue:
Comment: Unable to manage file: Jinja variable 'dict object' has no attribute 'macaddress'; line 9
---
[...]
ethernets:
{{ grains['interface_context'] }}:
dhcp4: {{ grains['dhcp4'] }}
dhcp6: {{ grains['dhcp6'] }}
addresses: [{{ grains['ipv4'] }}, "{{ grains['ipv6'] }}"]
{% if grains['macaddress'] %} <======================
match:
macaddress: {{ grains['macaddress'] }}
{% endif %}
routes:
- to: default
[...]
---
As the message indicates, the grain "macaddress" is missing, which I can confirm, it's not set for this minion. But What I do not understand is how I can simply check if this variable/grain exists at all within a jinja template?
I wouldn't expect this error to come up, as I actually wanted to catch it with the if statement.
Can somebody help?
Use get to return None instead of raising:
{% if grains.get('macaddress') is not none %}
Or if you want to treat "empty" values the same:
{% if not grains.get('macaddress') %}

Jinja2 whitespace controls doesn't work as expected with IF block

First of all: I read thoroughly documentation about whitespace control in Jinja documentation!
Have simple Jinja template:
New client has been created.
{% if not client.address %}!! Client does not have address{% endif %}
{% if not client.first_name or not client.last_name %}!! Client does not have first/last names{% endif %}
`C{{ client.id }} {{ client.full_name or "##MISSING##" }}
{{ client.status }}
When client hasn't address and first name it is rendering correct:
New client has been created.
!! Client does not have address
!! Client does not have first/last names
`C123 Some Name
pending
But when client has address or name:
New client has been created.
`C123 Some Name
pending
if expectedly produces empty lines.
Reason why I cannot user {%- if -%}: in this case second example rendering correctly BUT first example joins together both lines with "ifs".
Question: how to keep lines separate in the case then if conditions are true AND remove newlines when it falsy?
UPD: This is how I instantiate my Jinja environment:
from jinja2 import Environment, PackageLoader, select_autoescape
loader = PackageLoader("tally2bot", "templates")
env = Environment(
loader=loader,
autoescape=select_autoescape(["html", "xml", "jinja2"])
)
try this snippet below:
New client has been created.
{%- if not client.address %}
!! Client does not have address
{%- endif %}
{%- if not client.first_name or not client.last_name %}
!! Client does not have first/last names
{%- endif %}
`C{{ client.id }} {{ client.full_name or "##MISSING##" }}
{{ client.status }}
__ init__.py
from jinja2 import Environment, PackageLoader, select_autoescape
env = Environment(
loader=PackageLoader("app", "templates"),
autoescape=select_autoescape(["html", "xml", "jinja2"])
)
template = env.get_template('index.html')
print(template.render({'client': None}))
print('--------------------------------------------------------------------------------')
client = {
'id': 123,
'first_name': 'John',
'last_name': 'DOE',
'full_name': 'John DOE',
'address': 'CA',
'status': 'pending'
}
print(template.render({'client': client}))
result:
(venv) PS D:\dev\python\app> py .\__init__.py
New client has been created.
!! Client does not have address
!! Client does not have first/last names
`C ##MISSING##
pending
--------------------------------------------------------------------------------
New client has been created.
`C123 John DOE
pending

Accessing items in nested dicts and lists in Jinja2

Data:
primaries:
ca:
- 10.51.60.45
- 10.51.60.46
ny:
- 10.52.60.45
- 10.52.60.46
az:
- 10.53.60.45
- 10.53.60.46
I want a flattened list of all IP's(or a for loop which can iterate through just the IP's), but the cities ca and ny and az could be anything.
Ansible's extract filter, which extracts the value of a key from a container, makes this very simple.
{{ primaries | map('extract', primaries) | flatten }}
You can also directly use the dictionary's values() method, which is slightly less flexible (the extract approach allows you to filter the keys beforehand, which you can't do here.)
{{ primaries.values() | flatten }}
You just need to iterate through the keys of the dictionary.
{% for region, ips in primaries.items() %}
{% for ip in ips %}
{{ ip }}
{% endfor %}
{% endfor %}
Read the Jinja docs on for.
Using Ansible, you can get a flattened list of ips using the json_query filter:
List of ip addresses:
{% for addr in primaries|json_query('*[][]') %}
- {{ addr }}
{% endfor %}
This results in:
List of ip addresses:
- 10.51.60.45
- 10.51.60.46
- 10.52.60.45
- 10.52.60.46
- 10.53.60.45
- 10.53.60.46
Here's a runnable example:
- hosts: localhost
gather_facts: false
vars:
primaries:
ca:
- 10.51.60.45
- 10.51.60.46
ny:
- 10.52.60.45
- 10.52.60.46
az:
- 10.53.60.45
- 10.53.60.46
tasks:
- copy:
dest: addresses.txt
content: |
List of ip addresses:
{% for addr in primaries|json_query('*[][]') %}
- {{ addr }}
{% endfor %}
The json_query filter uses the JMESPath query language.

Ansible: Get all the IP addresses of a group

Let's imagine an inventory file like this:
node-01 ansible_ssh_host=192.168.100.101
node-02 ansible_ssh_host=192.168.100.102
node-03 ansible_ssh_host=192.168.100.103
node-04 ansible_ssh_host=192.168.100.104
node-05 ansible_ssh_host=192.168.100.105
[mainnodes]
node-[01:04]
In my playbook I now want to create some variables containing the IP addresses of the group mainnodes:
vars:
main_nodes_ips: "192.168.100.101,192.168.100.102,192.168.100.103,192.168.100.104"
main_nodes_ips_with_port: "192.168.100.101:3000,192.168.100.102:3000,192.168.100.103:3000,192.168.100.104:3000"
This is what I got so far:
vars:
main_nodes_ips: "{{groups['mainnodes']|join(',')}}"
main_nodes_ips_with_port: "{{groups['mainnodes']|join(':3000,')}}"
but that would use the host names instead of the IP addresses.
Any ideas how this could be done?
Update:
looking at the docs for a while, I think this would allow me to loop through all the ip adresses:
{% for host in groups['mainnodes'] %}
{{hostvars[host]['ansible_ssh_host']}}
{% endfor %}
But I just can't figure out how to create an array that holds all these IPs. So that I can use the |join() command on them.
Update2:
I just thought I had figured it out... but it turns out that you cannot use the {% %} syntax in the playbook... or can I?
Well in the vars section it didn't. :/
vars:
{% set main_nodes_ip_arr=[] %}
{% for host in groups['mesos-slave'] %}
{% if main_nodes_ip_arr.insert(loop.index,hostvars[host]['ansible_ssh_host']) %} {% endif %}
{% endfor %}
main_nodes_ips: "{{main_nodes_ip_arr|join(',')}}"
main_nodes_ips_with_port: "{{main_nodes_ip_arr|join(':3000,')}}"
I find the magic map extract here.
main_nodes_ips: "{{ groups['mainnodes'] | map('extract', hostvars, ['ansible_host']) | join(',') }}"
main_nodes_ips_with_port: "{{ groups['mainnodes'] | map('extract', hostvars, ['ansible_host']) | join(':3000,') }}:3000"
An alternative(idea comes from here):
main_nodes_ips: "{{ groups['mainnodes'] | map('extract', hostvars, ['ansible_eth0', 'ipv4', 'address']) | join(',') }}"
(Suppose the interface is eth0)
i came across this problem a while back and this is what i came up with (not optimal, but it works)
---
# playbook.yml
- hosts: localhost
connection: local
tasks:
- name: create deploy template
template:
src: iplist.txt
dest: /tmp/iplist.txt
- include_vars: /tmp/iplist.txt
- debug: var=ip
and the template file is
ip:
{% for h in groups['webservers'] %}
- {{ hostvars[h].ansible_ssh_host }}
{% endfor %}
This do the trick for me. Not relying on the interface name
- main_nodes_ips: "{{ groups['mainnodes'] | map('extract', hostvars, ['ansible_default_ipv4', 'address']) | join(',') }}"
- name: Create List of nodes to be added into Cluster
set_fact: nodelist={%for host in groups['mygroup']%}"{{hostvars[host].ansible_eth0.ipv4.address}}"{% if not loop.last %},{% endif %}{% endfor %}
- debug: msg=[{{nodelist}}]
- name: Set Cluster node list in config file
lineinfile:
path: "/etc/myfonfig.cfg"
line: "hosts: [{{ nodelist }}]"
as results you will have the following line in config file:
hosts: ["192.168.126.38","192.168.126.39","192.168.126.40"]
I got it to work on my own now. I'm not too happy about the solution, but it will do:
main_nodes_ips: "{% set IP_ARR=[] %}{% for host in groups['mainnodes'] %}{% if IP_ARR.insert(loop.index,hostvars[host]['ansible_ssh_host']) %}{% endif %}{% endfor %}{{IP_ARR|join(',')}}"
main_nodes_ips_with_port: "{% set IP_ARR=[] %}{% for host in groups['mainnodes'] %}{% if IP_ARR.insert(loop.index,hostvars[host]['ansible_ssh_host']) %}{% endif %}{% endfor %}{{IP_ARR|join(':3000,')}
I've done this by using ansible facts in a playbook.
This playbook takes ansible_all_ipv4_addresses list and ansible_nodename (which is actually fully qualified domain name), iterates through all hosts and saves the data in localpath_to_save_ips file on your localhost. You can change localpath_to_save_ips to the absolute path on your localhost.
---
- hosts: all
become: yes
gather_facts: yes
tasks:
- name: get ip
local_action: shell echo {{ ansible_all_ipv4_addresses }} {{ ansible_nodename }} >> localpath_to_save_ips
I found the "only way" to acceess other group's ip's, when any of the following is true:
some members are not bootstrapped by ansible yet
using serial
group is not part of playbook
Is as follows:
{% set ips=[] %}{% for host in groups['othergroup'] %}{% if ips.append(lookup('dig', host)) %}{% endif %}{% endfor %}{{ ips }}
Requires dnspython on the machine running ansible, install via
sudo apt-get install python-dnspython
If anyone knows a better way given the conditions, I'd love to get rid of this abomination.
this is what I did in order to not be relied on eth0 (thanks to ADV-IT's answer):
- name: gathering facts
hosts: mainnodes
gather_facts: true
- hosts: mainnodes
tasks:
- name: Create List of nodes
set_fact: nodelist={%for host in groups['mainnodes']%}"{{hostvars[host]['ansible_env'].SSH_CONNECTION.split(' ')[2]}}"{% if not loop.last %},{% endif %}{% endfor %}
I ran into a similar problem getting the IP address of a node in another group.
Using a construct like:
the_ip: "{{ hostvars[groups['master'][0]]['ansible_default_ipv4'].address }}"
works only when running the group master, which was not part of my playbook (I was running on localhost).
I have overcome the problem by adding an extra play to playbook, like:
- hosts: master
gather_facts: yes
become: no
vars:
- the_master_ip: "{{ hostvars[groups['master'][0]]['ansible_default_ipv4'].address }}"
tasks:
- debug: var=the_master_ip
- set_fact: the_ip={{ the_master_ip }}
After which I can use the the_ip in the next play of the playbook.
This may also solve the abomination mentioned by #Petroldrake ?
##Just fetch Ip's using -ansible_default_ipv4.address- & redirect to a local file & then use it
name: gathering_facts
hosts: hosts
gather_facts: true
tasks:
name: Rediret to the file
shell: echo "{{ansible_default_ipv4.address}}" >>ipss.txt
delegate_to: localhost

Jinja variable is undefined

I'm trying to build the hosts file on my servers using salt. In some of the servers, the eth0 network interface has the inet set and in some others is the bond0 interface.
In the init.sls i have:
/etc/hosts:
file.managed:
- source: salt://configs/etc/hosts/hostsfile
- user: root
- group: root
- mode: 644
- template: jinja
- context:
{% for host, interface in salt['mine.get']('*', 'network.interfaces').items() %}
{% if interface['bond0'].has_key('inet') %}
ip: {{ salt['network.interfaces']()['bond0']['inet'][0]['address'] }}
{% else %}
ip: {{ salt['network.interfaces']()['eth0']['inet'][0]['address'] }}
{% endif %}
{% endfor %}
hostname: {{ salt['network.get_hostname']() }}
And in my hosts file that is set above in the "- source", i have:
{{ ip }} {{ hostname }}
Then, when i run a state.highstate from the salt master, i get an error saying:
SaltRenderError: Jinja variable 'ip' is undefined; line 97
It seems like that the salt function that retrieves the network interface, does not work when it is inside the jinja for loop(or i'm doing something wrong).
I'm saying that because last line where it returns the hostname, works just fine.
What am i doing wrong here ? I'm suspecting that the if condition is not met and thus the "ip" variable never gets assigned a value.
Thank you,