In this article by Rishabh Das, author of the book Extending Ansible, we will deep dive into what Ansible plugins are and how you can write your own custom Ansible plugin. The article will discuss the different types of Ansible plugins in detail and explore them on a code level. The article will walk you through the Ansible Python API and using the extension points to write your own Ansible plugins.
(For more resources related to this topic, see here.)
Lookup plugins are designed to read data from different sources and feed them to Ansible. The data source can be either from the local filesystem on the controller node or from an external data source. These may also be for file formats that are not natively supported by Ansible.
If you decide to write your own lookup plugin, you need to drop it in one of the following directories for Ansible to pick it up during the execution of an Ansible playbook.
By default, a number of lookup plugins are already available in Ansible. Let's discuss some of the commonly used lookup plugins.
This is the most basic type of lookup plugin available in Ansible. It reads through the file content on the controller node. The data read from the file can then be fed to the Ansible playbook as a variable. In the most basic form, usage of file lookup is demonstrated in the following Ansible playbook:
---
- hosts: all
vars:
data: "{{ lookup('file', './test-file.txt') }}"
tasks:
- debug: msg="File contents {{ data }}"
The preceding playbook will read data off a local file, test-file.txt, from the playbook's root directory into a data variable. This variable is then fed to the task : debug module, which uses the data variable to print it onscreen.
The csvfile lookup plugin was designed to read data from a CSV file on the controller node. This lookup module is designed to take in several parameters, which are discussed in this table:
Parameter | Default value | Description |
file | ansible.csv | This is the file to read data from |
delimiter | TAB | This is the delimiter used in the CSV file, usually ','. |
col | 1 | This is the column number (index) |
default | Empty string | This returns this value if the requested key is not found in the CSV file |
Let's take an example of reading data from the following CSV file. The CSV file contains population and area details of different cities:
File: city-data.csv
City, Area, Population
Pune, 700, 2.5 Million
Bangalore, 741, 4.3 Million
Mumbai, 603, 12 Million
This file lies in the controller node at the root of the Ansible play. To read off data from this file, the csvfile lookup plugin is used. The following Ansible play tries to read the population of Mumbai from the preceding CSV file:
Ansible Play – test-csv.yaml
---
- hosts: all
tasks:
- debug:
msg="Population of Mumbai is {{lookup('csvfile', 'Mumbai
file=city-data.csv delimiter=, col=2')}}"
The dig lookup plugin can be used to run DNS queries against Fully Qualified Domain Name (FQDN). You can customize the lookup plugin's output using the different flags that are supported by the plugin. In the most basic form, it returns the IP of the given FQDN.
This plugin has a dependency on the python-dns package. This should be installed on the controller node.
The following Ansible play explains how to fetch the TXT record for any FQDN:
---
- hosts: all
tasks:
- debug: msg="TXT record {{ lookup('dig', 'yahoo.com./TXT') }}"
- debug: msg="IP of yahoo.com {{lookup('dig', 'yahoo.com',
wantlist=True)}}"
The preceding Ansible play will fetch the TXT records in step one and the IPs associated with FQDN yahoo.com in second.
It is also possible to perform reverse DNS lookups using the dig plugin with the following syntax:
- debug: msg="Reverse DNS for 8.8.8.8 is {{ lookup('dig', '8.8.8.8/PTR')
}}"
The ini lookup plugin is designed to read data off an .ini file. The INI file, in general, is a collection of key-value pairs under defined sections. The ini lookup plugin supports the following parameters:
Parameter | Default value | Description |
type | ini | This is the type of file. It currently supports two formats: ini and property. |
file | ansible.ini | This is the name of file to read data from. |
section | global | This is the section of the ini file from which the specified key needs to be read. |
re | false | If the key is a regular expression, we need to set this to true. |
default | Empty string | If the requested key is not found in the ini file, we need to return this. |
Taking an example of the following ini file, let's try to read some keys using the ini lookup plugin. The file is named network.ini:
[default]
bind_host = 0.0.0.0
bind_port = 9696
log_dir = /var/log/network
[plugins]
core_plugin = rdas-net
firewall = yes
The following Ansible play will read off the keys from the ini file:
---
- hosts: all
tasks:
- debug: msg="core plugin {{ lookup('ini', 'core_plugin
file=network.ini section=plugins') }}"
- debug: msg="core plugin {{ lookup('ini', 'bind_port
file=network.ini section=default') }}"
The ini lookup plugin can also be used to read off values through a file that does not contain sections—for instance, a Java property file.
There are times when you need to perform the same task over and over again. It might be the case of installing various dependencies for a package or multiple inputs that go through the same operation—for instance, while checking and starting various services. Just like any other programming language provides a way to iterate over data to perform repetitive tasks, Ansible also provides a clean way to carry out the same operation. The concept is called looping and is provided by Ansible lookup plugins.
Loops in Ansible are generally identified as those starting with “with_”. Ansible supports a number of looping options. A few of the most commonly used are discussed in the following section.
This is the simplest and most commonly used loop in Ansible. It is used to iterate over an item list and perform some operation on it. The following Ansible play demonstrates the use of the with_items lookup loop:
---
- hosts: all
tasks:
- name: Install packages
yum: name={{ item }} state=present
with_items:
- vim
- wget
- ipython
The with_items lookup loop supports the use of hashes in which you can access the variables using the .<keyname> item in the Ansible playbook. The following playbook demonstrates the use of with_item to iterate over a given hash:
---
- hosts: all
tasks:
- name: Create directories with specific permissions
file: path={{item.dir}} state=directory mode={{item.mode | int}}
with_items:
- { dir: '/tmp/ansible', mode: 755 }
- { dir: '/tmp/rdas', mode: 755 }
The preceding playbook will create two directories with the specified permission sets. If you look closely while accessing the mode key from item, there exists a | int filter. This is a jinja2 filter, which is used to convert a string to an integer.
This has the same implementation as that in any other programming language. It executes at least once and keeps executing unless a specific condition is reached, as follows:
< Code to follow >
This article covered some already available Ansible lookup plugins and explained how these can be used. This section will try to replicate a functionality of the dig lookup to get the IP address of a given FQDN. This will be done without using the dnspython library and will use the basic socket library for Python. The following example is only a demonstration of how you can write your own Ansible lookup plugin:
import socket
class LookupModule(object):
def __init__(self, basedir=None, **kwargs):
self.basedir = basedir
def run(self, hostname, inject=None, **kwargs):
hostname = str(hostname)
try:
host_detail = socket.gethostbyname(hostname)
except:
host_detail = 'Invalid Hostname'
return host_detail
The preceding code is a lookup plugin; let’s call it hostip.
As you can note, there exists a class named LookupModule. Ansible identifies a Python file or module as a lookup plugin only when there exists a class called LookupModule. The module takes an argument hostname and checks whether there exists an IP corresponding to it—that is, whether it can be resolved to a valid IP address. If yes, it returns the IP address of the requested FQDN. If not, it returns Invalid Hostname.
To use this module, place it in the lookup_modules directory at the root of the Ansible play. The following playbook demonstrates how you can use the hostip lookup just created:
---
- hosts: all
tasks:
- debug:
msg="{{lookup('hostip', item, wantlist=True)}}"
with_items:
- www.google.co.in
- saliux.wordpress.com
- www.twitter.com
The preceding play will loop through the list of websites and pass it as an argument to the hostip lookup plugin. This will in turn return the IP associated with the requested domain. If you notice, there is an argument wantlist=True also passed while the hostip lookup plugin was called. This is to handle multiple outputs; that is, if there are multiple values associated with the requested domain, the values will be returned as a list. This makes it easy to iterate over the output values.
This article picked up on how the Ansible Python API for plugins is implemented in various Ansible plugins. The article discussed various types of plugins in detail, both from the implementation point of view and on a code level. The article also demonstrated how to write sample plugins by writing custom lookup plugins. By the end of this article, you should be able to write your own custom plugin for Ansible.