Writing a Filter Plugin in Ansible
Writing a Filter Plugin in Ansible
You might find that you're doing the same kind of data-munging in multiple places. There are many cases where you'd want to pull this into a little named package:
- It requires logic which can't be expressed simply in basic jinja filters
- It's common and used in many places, so there's lots of duplication
- It's uncommon and you can never remember the requirements
- It's actually complicated
- You just want to name what these 3 filters do together
Basic outline
The basic outline of a plugin is as follows:
import re
def storage_account(name): # 3
return re.sub("[^a-z0-9]", "", name)[:24]
class FilterModule(object): # 1
def filters(self): # 2
return {"storage_account": storage_account} # 4
- Create a class FilterModule
- Create a function
filters
- Define your function
- Return a dict with k-vs of
filter_name:function_reference
Documenting your plugin
If you want your documentation to work with the Ansible tooling (showing up nicely in the docs, working with ansible-doc
), you can't use the standard ansible documentation, since Filter plugins don't show up as an option you can select documentation for. Go figure. You can still try to use the standard documentation features, though.
You can include only the sections of the documentation that you want to appear. Here's the full referece for the version this article was written to. For convenience, here they are:
- Shebang & encoding
- Copyright
- DOCUMENTATION (includes module parameters)
- EXAMPLES
- RETURN
These have to be python strings which contain the YAML. This makes it kinda gross to work on, since you have no YAML syntax highlighting to help you. Here's a stub python file (feel free to change the license line):
{% raw %}
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) {{ year }}, {{ Your Name }} <{{ your email }}>
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
DOCUMENTATION = r"""
module:
short_description:
description:
-
version_added: "0.1.0"
options:
"""
EXAMPLES = r"""
"""
RETURN = r"""
{{ key }}:
desctiption:
returned:
type:
sample:
"""
{% endraw %}
Common things you'd want to do
Using an existing filter
If you want to use an existing filter plugin (perhaps to format your output), you can just import them from the Ansible package:
from ansible.plugins.filter.core import to_json, quote
Including other parameters in your filter
If you want to have other parameters in your filter, you can just do that
from ipaddress import ip_network
def ip_in_snet(snet, ip):
_snet = ip_network(snet)
_ip = int(ip)
return _snet[_ip]
class FilterModule(object):
def filters(self):
return {"ip_in_snet": ip_in_snet}
and then use that like you'd expect, more or less
"{{ '10.0.1.0/24' | ip_in_snet(10) }}"
Building an extractor
Sometimes you can't remember the sequence of keys you need to extract that thing you want from the huge JSON blob. You can write a simple extractor:
DOCUMENTATION = r"""
name: extract_connectionstring
short_description: extracts the connection stirng from azure_rm_storageaccount_info
description:
- extracts the connection stirng from M(azure_rm_storageaccount_info)
- make sure to use the option C(show_connection_string: true) on the M(azure_rm_storageaccount_info) invocation
options:
rm_info:
description:
- The result of azure_rm_storageaccount_info
type: object
required: True
endpoint:
description:
- The storageaccount endpoint to get the connectionstring for
- if blank, will fetch for blob
- use "key" to get the key itself
choices: ["blob", "file", "queue", "table", "key"]
required: False
default: blob
"""
EXAMPLES = r"""
- name: get storageaccount info
azure_rm_storageaccount_info:
name: "{{ storage_acount_name }}"
resource_group "{{ rg }}"
show_connection_string: true # important !
register: storageaccount_raw
- name: get blob connection string
set_fact:
blob_connectionstring: "{{ storageaccount_raw | extract_connectionstring }}
- name: get connection string for something other than blob
set_fact:
table_connectionstring: "{{ storageaccount_raw | extract_connectionstring('table') }}
- name: get account key
set_fact:
connectionstring: "{{ storageaccount_raw | extract_connectionstring('key') }}
"""
def extract_connectionstring(rm_info, endpoint="blob"):
if endpoint == "key":
return rm_info["storageaccounts"][0]["primary_endpoints"]["key"]
else:
return rm_info["storageaccounts"][0]["primary_endpoints"][endpoint][
"connectionstring"
]
class FilterModule(object):
def filters(self):
return {"extract_connectionstring": extract_connectionstring}
Abusing filters as functions
Let's say that you want to have some functions assemble things, like you would with some proper typed data modelling. I'm not saying this is a good idea, but you can use functions as the closest thing to that (it's not really close). For an example, I always forget the little fiddly bits of assembling an Azure Storageaccount Lifecycle Policy. I could do the following:
from _includes.resources.ansible_plugins.filter.extractor import EXAMPLES
EXAMPLES = r"""
- name: format lifecycle policy
set_fact:
autodelete_rule: "{{ "autodelete" | storageaccount_lifecycle_rule(filters, blob_actions) }}
vars:
blob_actions:
- {"delete":{"daysAfterModificationGreaterThan": 30}}
filters:
- {"blobTypes":["blockBlob"],"prefixMatch":["my_container"]}
"""
from typing import Dict
def rule(
name,
filters,
actions_blob: Dict[str, Dict] = None,
actions_version: Dict[str, Dict] = None,
):
return {
"enabled": True,
"name": str(name),
"type": "Lifecycle",
"definitions": {
"actions": {"baseBlob": actions_blob, "versions": actions_version,},
"filters": filters,
},
}
class FilterModule(object):
def filters(self):
return {"storageaccount_lifecycle_rule": rule}
And yes, you can also build filters to construct those filters
and blob_actions
, and make a terrible filter chain. But maybe don't and just write a one-off plugin.