If you’re using Ansible to manage configuration files on your servers, you’re probably familiar with the
{{ ansible_managed }}
tag .
This tag is well behaved from an idempotentcy perspective, but in the nitty-gritty of managing servers where there’s a chance of interaction by humans, this built-in tag leaves a lot to be desired.
Enter Ansible filter plugins, and a little bit of magic in group_vars.
This filter plugin will obtain (according to git pretty-formats) the commit hash, author, and date of the last commit to the file provided by the filter plugin structure:
#!/usr/bin/python
# this should be saved to ./filter_plugins/get_last_commit_to_file.py
class FilterModule(object):
def filters(self):
return {
'get_last_commit_to_file': self.get_last_commit_to_file
}
def get_last_commit_to_file(self, file_path):
import os
try: # py3
from shlex import quote
except ImportError: # py2
from pipes import quote
stream = os.popen('git log -n 1 --pretty=format:\'%h by %an at %aD\' -- ' + quote(file_path))
output = stream.read()
return output
So, now we can define two new variables in our group_vars/all.yml
file: ansible_workspace_git_url
and ansible_managed_custom
.
Notice the use of our custom filter plugin – we can pipe the variable template_path variable (which Ansible gives to us for free) into our filter plugin to obtain a string about the git history of our file!
ansible_workspace_git_url: "https://github.com/myrepo"
ansible_managed_custom: "
Ansible Managed for {{ inventory_hostname }} ({{ansible_system_vendor}}::{{ansible_product_name}}::{{ansible_product_serial}}) \n
Ansible Git Repository URL: {{ ansible_workspace_git_url }}\n
Inventory File: {{ inventory_file|replace(inventory_dir,'') }}@{{ inventory_file | get_last_commit_to_file }}\n
Template file: {{ template_path|replace(inventory_dir,'') }}@{{ template_path | get_last_commit_to_file }}\n
Role Context: {{ role_name | default('None') }}\n
Rendered Template Target: {{ template_destpath }}"
We now have available for use inside any template a nicely formatted (but not necessarily idempotent) header which gives any readers DETAILED information including:
- The hostname and BIOS serial number of the host to which this file was rendered
- The git repository of the playbook responsible for this template
- The filename of the template used to render this file
- The hash of the last commit which affected the template used to render this file
- The filename of the inventory supplying variables to render this template
- The hash of the last commit which affected the inventory supplying variables to render this template
- The target path on the target server to where this file was rendered
Now, anytime I’m templating out a file (well, at least any file that supports multiline comments), I can just include our new ansible_managed_custom
variable:
<!--
{{ ansible_managed_custom }}
-->
By switching to this header format, I was able to better assist my teammates with identifying why a template was rendered in a particular fashion, as well as provide them details about how to fix any problems they may find. This template format also helps answer questions about whether the rendered file was _copied_ from one server to another (possibly in a migration) and whether the file is likely to still be under Ansible management.