Jinja2 & Ansible - Loop over lines in several text files - ansible

I'm using Ansible to retrieve a list of website we host over 50+ servers. I'm using a SQL query to store in a text file named as hostname_websites.txt using Ansible.
The next step is to build a prometheus (blackbox exporter) configuration file using that data.
Each line of each text file represent an URL. For the sake of representation I'll declare that the URL is $URL and the hostname is $HOSTNAME.
I need some jinja2 magic to generate a single yaml file as :
{% for site in *** each text file *** %}
- targets: ['{{ $URL }}']
labels: {'node': '{{ $HOSTNAME }}'}
{% endfor %}
Given that in the end I need to have a config line containing each time the URL which is a line in text file, and the hostname as a label which is in the txt filename.
Every hostname is in a group called production in Ansible, so I tried looping over that group using jinja2 as follow:
{% for host in group['production'] %}
{{ lookup('file', '{{ host }}_websites.txt).splitlines()' }}
{% endfor %}
That gave me some jinja2 parsing error, as if it wasn't rendering the for loop at all.
I stopped here, in the spirit that I would build by configuration skel around this statement.

There are two problems with your template that jump out immediately:
There is no variable group in Ansible (unless you've created one explicitly); you probably want groups.
You never nest Jinja {{...}} markers. If you want to generate a string containing a variable value, you can use string concatenation:
lookup('file', host ~ '_websites.txt')
Or string formatting:
lookup('file', '%s_websites.txt' % host)
Assuming that our inventory has a host node0 in the group production and that there exists a file node0_websites.txt with the following content:
http://stackoverflow.com/
http://ansible.com/
http://stackexchange.com/
Then running this playbook:
- hosts: localhost
gather_facts: false
tasks:
- copy:
dest: output.txt
content: |
{% for host in groups['production'] %}
# Host: {{ host }}
{% for url in lookup('file', host ~ '_websites.txt').splitlines() %}
- {{ url }}
{% endfor %}
{% endfor %}
Generates the following content in output.txt:
# Host: node0
- http://stackoverflow.com/
- http://ansible.com/
- http://stackexchange.com/

Related

can I run a shell command in jinja2 template in ansible

This is a playbook that connects with all the servers in my inventory file, and makes a note of the server ip and mount point information of hosts where mount point usage exceeds 80% and it writes to a text file on the localhost (ansible-controller).
- hosts: all
tasks:
- shell:
cmd: df -h | sed 's/%//g' | awk '$5 > 80 {if (NR > 1) print $5"%",$6}'
register: disk_stat
- debug:
var: disk_stat
- file:
path: /home/app/space_report_{{ td }}.txt
state: touch
run_once: true
delegate_to: localhost
- shell: echo -e "{{ ansible_host }} '\n' {{ disk_stat.stdout_lines| to_nice_yaml }}" >> /home/thor/space_report_{{ td }}.txt
args:
executable: /bin/bash
delegate_to: localhost
I was wondering if I could create a jinja2 template and bring the playbook down to one task. I am stuck at integrating a shell command inside the jinja2 template and I am not sure if it is possible. Please advise.
- hosts: all
tasks:
- template:
src: monitor.txt.j2
dest: /home/app/playbooks/monitor.txt
delegate_to: localhost
monitor.txt.j2
{% for host in groups['all'] %}
{{ hostvars[host].ansible_host }}
--shell command--
{% endfor %}
As I say in my comment under your question, while it is possible to use shell or command modules, Ansible is a configuration / automation tool, so it's better to forget your shell coding / logic to use native ansible functionalities, that'll ease the tasks / playbook writing.
For instance, it's no needed to do a df because ansible when connecting will gather facts about the target, including devices and their capacity and current usage so you can use that directly.
For the jinja question, you can use the module copy and pass directly jinja code in the option content on this module:
- name: Trigger a tasks on hosts to gather facts
hosts: all
tasks:
- copy:
dest: /home/app/playbooks/monitor.txt
content: |
{% for host in groups['all'] %}
{% for dev in hostvars[host].ansible_mounts %}
{% if (dev.block_used / dev.block_total * 100 ) > 80 %} {{ dev.block_used / dev.block_total * 100 }} {{ dev.mount }} {% endif %}
{% endfor %}
{% endfor %}
run_once: true
delegate_to: localhost

Ansible Jinja2 template for loop

I have two linux servers:
- server1: ip: 10.241.55.6, hostname: server1
- server2: ip: 10.242.55.7, hostname: server2
I have created an ansible inventory file named servers with the content bellow:
[IC]
10.241.55.6
10.241.55.7
Now I have created this jinja2 inventory template file: test.j2 with this content:
[IC]
{% for hostip in groups['IC'] %}
{% if hostip == ansible_default_ipv4.address %}
{{ ansible_default_ipv4.address }} default_hostname={{ ansible_nodename }}
{{ ansible_default_ipv4.address }} default_hostname={{ ansible_nodename }}
{% endif %}
{% endfor %}
And I'm running this ansible playbook:
---
- name: Generate portal inventory file
hosts: all
tasks:
- name: Generate inventory
delegate_to: localhost
template:
src: inventory/test.j2
dest: inventory/test
The command is: ansible-playbook -i inventory/servers generate-inventory.yml
The final goal is that ansible connects to each of the servers from the inventory files and then based on the jinja2 inventory template, it creates a new inventory file with this format:
[IC]
10.241.55.6 default_hostname=hostname_of_the_server_with_that_ip
and so on...
The issue here with the for loop is that all the entries are with the same server ip (while I should have an entry for each of the servers with their respective hostnames):
[IC]
10.241.55.6 default_hostname=server1
10.241.55.6 default_hostname=server2
What I'm missing here? Also if there is any other better way to achieve this please let me know.
You're using the same variable twice in the template...
{{ ansible_default_ipv4.address }} default_hostname={{ ansible_nodename }}
{{ ansible_default_ipv4.address }} default_hostname={{ ansible_nodename }}
...so of course you're getting two identical lines. It sounds like you want to access the per-host value of this variable, which means you need to access it via hostvars.
Maybe something like this:
[IC]
{% for host in groups['IC'] %}
{{ hostvars[host].ansible_default_ipv4.address }} default_hostname={{ hostvars[host].ansible_nodename }}
{% endfor %}

How to use hosts in my playbook for initiating my template

I have a playbook to initiate a conf to servers that requires hostname. I want to pass my hosts that define in playbook as group so as to loop the variables in my jinja2 template. And I don't want to set the hosts in vars(because that was already define in playbook, I don't know the reason for re-define)
For example:
host file:
[test_servers]
t1
t2
t3
[test2_servers]
t2
t4
playbook:
- hosts: test_servers
tasks:
- name: generate my conf
template:
src: templates/temp.conf.j2
dest: "test.conf"
force: True
vars:
hosts: test_servers # So far I need to declare the var here duplicately, I've group my server in host file and I just want to use the current group.
temp.conf
....
{% for host in groups[hosts] %}
Entry.{{ loop.index }} = {{ host }}
{% endfor %}
....
I wonder if there is a better way to pass the hosts that set in playbook to my jinja2 template so I can re-use the playbook for different hosts. For example, I just need to reset the playbook hosts to test2, no need to rewrite the vars.
There is no special variable with the name of the group, but there is ansible_play_hosts_all
List of all the hosts that were targeted by the play
Remove vars and use ansible_play_hosts_all in the template
- hosts: test_servers
tasks:
- name: generate my conf
template:
src: templates/temp.conf.j2
dest: "test.conf"
force: True
{% for host in ansible_play_hosts_all %}
Entry.{{ loop.index }} = {{ host }}
{% endfor %}

Get Number from hostname in Ansible

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"

How to create dynamic list in yaml?

I am trying to render a template. I have to create a list as
host:
- '111.222.333.444'
- '555.666.777.888'
which has to be taken from another host file.
I am using something like this:
{% for host in groups['hostgroup'] %}
host: {{ host }}
{% endfor %}
What is the correct way to achieve the result?
Assuming you wish to declare a variable in your inventory called host which contains a list of ip addresses, you can try:
host: {{ groups['hostgroup'] }}
Or you could skip declaring this variable and use {{ groups['hostgroup'] }} directly wherever you plan to use {{ host }}
Look at add-quotes-join thread and this filter plugin if you want quotes.
EDIT:
Assuming you are rendering a template, using the ansible template module
host:
{% for host in groups['hostgroup'] %}
- '{{ host }}'
{% endfor %}

Resources