The Ansible Automation Platform is a great tool for automation and configuration deployment. In General. That’s why I am also using it on my local clients to keep the configuration in sync and even do the most minuscule changes.

Very rarely I implement changes that do not need to be implemented right away or that will have to be reversed later.

The usual workflow for this is to wait until that moment, implement the change and roll it out. Or at least implement the change and schedule the Ansible run rather sooner than later.

This has some minor drawbacks: I either have to wait or to remember not to run Ansible when the code already has been updated, but is not supposed to be rolled out.

My particular requirement for this case was to remove a script and a cron-job for an advent calendar after the 24. of December. One might think that this also triggered the reason for this article being published in December, but this was purely coincidental. Back to the topic.

Out of the box Ansible does not offer a time-controlled and simple way of scheduling tasks other than capsuling them into separate playbooks and run them at the given time (with e.g. Ansible Tower). The idea is: There’s a change, deploy it now.

So my question was: How would I implement such a conditional trigger in a way, that a) is easy and simple to do and b) I would still understand in a couple of months what has been done.

When it gets that complex and too complex for plain Ansible syntax, Jinja templates usually can provide an answer. The complex structures for handling and manipulating data like that, quickly becomes ugly though.

This is a code example I just quickly drafted to prove my point:

---
- hosts: all
  gather_facts: false
  tasks:
    - name: JINJA EXAMPLE
      set_fact:
        timeywimey: >-
          {% set return_value = 0 -%}
          {% set return_value = ( ("1970-01-01" | to_datetime('%Y-%m-%d')) - (ansible_date_time.date | to_datetime('%Y-%m-%d')) ).total_seconds() | int -%}
          {% if return_value < 0 %}True{% else -%}False{% endif -%}

    - debug:
        msg: "Random output"
      when: timeywimey

In this playbook the variable timeywimey is set to either True or False, depending on the date calculation. If the provided date lies in the past from the execution point, then the return value is True and the debug module will output Random output.

In the back of my head there’s always the mantra: “If you find yourself programming in Ansible, you are doing it wrong.”. And I think there’s something to it.

After going crazy in Jinja, the next level is to write custom Filters for Ansible. I do this on occasion when I cannot solve a problem with Ansible in a satisfactory manner and this solution would certainly not please my future me.


Implementing filters is a rather simple matter in Ansible.

You need to create a file for your custom filter, extend the Ansible class FilterModule, write the methods you require to handle the filter input and connect the method with the filter name you want to use.

The basic structure for a file with custom filter(s) looks like this:

# file: filter_plugins/example.py
class FilterModule:

    def foo(self, parameter1, parameter2):
       """Custom method to handle the filter input."""
       do_some_magic(parameter1)
       do_some_magic(parameter2)
       return True

    def filters(self):
        """Ansible method for assigning the filter name to the method."""
        return {
            "new_filter": self.foo,
            }

The custom filter new_filter becomes available, when the file example.py is Placed within the correct folder in the file-system for Ansible to detect and integrate. Using it in an Ansible task it then trivial.

---
# Example Ansible play
- name: EXAMPLE TASK
  debug:
    msg: "Random output"
  when: "'parameter1, parameter2' | new_filter"

With that approach I have added a couple of custom filters to ensure tasks are only executed when a certain “timely” condition is met.

I will just pull out one example filter to show the approach. The complete code is available as GitHub Project.

class FilterModule:

    def is_past(self, datestring):
        """Checks if a given datestring lies in the past."""
        check_date = self.get_dates(datestring)["check_date"]
        now_date = self.get_dates(datestring)["now_date"]
        if check_date < now_date:
            return True
        return False

    def filters(self):
        return {
            "is_past": self.is_past
        }

Let’s just quickly skim through the code. I have numbered the lines to make it easier to follow:

  • [3] I define a method is_past with a single parameter datestring. I kind of expect a date in the format YYYY-mm--dd here for the parameter. The purpose of the method is to compare two dates, datestring and right-now and figure out if the date in datestring lies in the past.
  • [5-6] Using a method called get_dates() I create two objects of datetime and return them as fields in a dictionary. I do this in each line, once for each object. The first field check_date contains the datetime object of datestring, the second field now_date contains the datetime object of the current date.
  • [7] The if-condition compares these two datetime objects and returns True if the object in check_date is smaller than now_date; it therefore lies in the past.
  • [9] The last line then returns False if the if-condition hasn’t been met.
  • [11-14] These lines assign the method self.is_past to a filter with the same name. They are required by the Ansible Framework when adding custom filters.

With values instead of python variables, the method is_past would look like this:

# executed on 2022-12-01
# otherwise rubbish python-syntax
def is_past(self, '1970-01-01'):
    object:'1970-01-01' = self.get_dates('1970-01-01')["check_date"]
    object:'2022-12-01' = self.get_dates('2022-12-01')["now_date"]
    if object:'1970-01-01' < object:'2022-12-01':
        return True
    return False

Integrating this in Ansible the first of the following tasks is executed, while the second one is skipped.

---
- name: EXAMPLE TASK 01
  debug:
    msg: "This will be executed."
  when: "'1970-01-01' | is_past "

- name: EXAMPLE TASK 02
  debug:
    msg: "This will never show."
  when: "'2170-01-01' | is_past "

With the custom filter in place, the example from the beginning looks now like this:

---
- hosts: all
  gather_facts: false
  tasks:
    - debug:
        msg: "Random output"
      when: "'1970-91-91' | is_past"

A filter like this creates a clear and concise condition. Even without knowing what the filter actually does, I have an idea about why a certain task is running or not.

Creating a custom filter is simple, but there are some pitfalls:

  1. You should choose a good name. My first idea was to use the schedule as filter name. While this is somehow explanatory in itself, I decided against it. Providing additional parameters to configure different time periods, etc. would have made the usage too complex. Instead I split up a single filter into multiple ones: is_past, is_today, is_future, is_today_or_past, … you get the idea. Check out the GitHub Project for more filters like that.
  2. Do not over-optimise the code. I could easily reduce the method is_past from the example just down to three lines: the if-condition and the two return statements. Hell, probably one line would do. But I wanted to make sure I can quickly understand the code even next year. So I simply assign the dates first and then run the comparison.
  3. The only filter I needed personally was is_past to solve my case this time. But I totally see the use-cases for the other filters as well, so I have added them right in. Except is_past_or_future. That one is on the to-do list and I am sure I will come back to me at some point. Look at it as an invitation to contribute.
  4. Simplicity * n did not turn out to be easy. Date-comparison in python is quite easy. Putting it in a Ansible filter is easy. Adding support for different timestamp formats is easy. All together became quite complex. That’s why I added tests as well. These cover many valid input cases and they made the developing and testing far easier than including the filter into an Ansible run. I do not remember how many times I discovered some issue using the tests, while I thought the code should work now. When I added it to Ansible in the end, it just worked.

The complete filter is available in the GitHub Project ansible_plugin_filters on GitHub. Feel free to use it.

Daniel Buøy-Vehn

Senior Systems Consultant at Redpill Linpro

Daniel works with automation in the realm of Ansible, AWX, Tower, Terraform and Puppet. He rolls out mainly to our customer in Norway to assist them with the integration and automation projects.

Thoughts on the CrowdStrike Outage

Unless you’ve been living under a rock, you probably know that last Friday a global crash of computer systems caused by ‘CrowdStrike’ led to widespread chaos and mayhem: flights were cancelled, shops closed their doors, even some hospitals and pharmacies were affected. When things like this happen, I first have a smug feeling “this would never happen at our place”, then I start thinking. Could it?

Broken Software Updates

Our department do take responsibility for keeping quite a lot ... [continue reading]

Alarms made right

Published on June 27, 2024

Just-Make-toolbox

Published on March 22, 2024