Script templates

Script templates #

I have a few rules of thumb I use when creating script templates. Following these makes scripts readable on the deployed host, and manageable in Ansible.

Keep Ansible variables in a block at the top of the script #

Don’t sprinkle Ansible variables throughout a templated script. Instead, keep them at the top of the file where they’re easy to find and update. This makes it immediately clear what in the script is controlled by Ansible’s templating vs the script itself, especially when looking at the script on the remote host after it has been provisioned by Ansible.

I also recommend using Ansible variables that are prefixed with the role name, shown here.

Here’s a toy script that does this:

#!/usr/bin/env python3

FOOBAR_MODE = "{{ rolename_foobar_mode }}"
GEEWIZ_TYPE = "{{ rolename_geewiz_type }}"


def run():
    if FOOBAR_MODE == "production":
        outfile = f"/var/foobar/production/{GEEWIZ_TYPE}"
    else:
        outfile = f"/tmp/{GEEWIZ_TYPE}"
    with open(outfile) as fp:
        fp.write("enable_example_asdf")


run()

Separate generic scripts (files) from wrapper scripts (templates) #

When writing longer scripts, you should make them generic enough to be copied in a copy task. They should not have Ansible variables in them, or use the template task. Instead, they should take runtime configuration, which can be templated instead.

That templated runtime configuration might be a config file, a wrapper script, environment variables, or whatever is appropriate. I tend to use a wrapper script.

In the following example long script, note that it takes many command-line arguments, but doesn’t have any Ansible variables. This pattern scales up to very long scripts easily.

#!/usr/bin/env python3

"""Imagine this little example script is really long and takes lots of arguments."""

import argparse
import sys
import typing


def parseargs(arguments: typing.List[str]):
    """Parse program arguments"""
    parser = argparse.ArgumentParser(description="Example program")
    # I've filled it with dummy flags generated by the very useful username-generator package
    # <https://github.com/awesmubarak/username_generator_cli>
    parser.add_argument("--administrative-statement")
    parser.add_argument("--comprehensive-actor")
    parser.add_argument("--pleasant-student")
    parser.add_argument("--emotional-president")
    parser.add_argument("--numerous-army")
    parser.add_argument("--recent-platform")
    parser.add_argument("--competitive-tooth")
    parser.add_argument("--obvious-oven")
    parser.add_argument("--medical-ladder")
    parser.add_argument("--unlikely-tradition")
    parser.add_argument("--available-queen")
    parser.add_argument("--afraid-county")
    parsed = parser.parse_args(arguments)
    return parsed


def main(*arguments):
    """Main program"""
    parsed = parseargs(arguments[1:])
    print(f"Imagine if I did something with all these arugments I parsed: {parsed}")


if __name__ == "__main__":
    sys.exit(main(*sys.argv))

In the following example wrapper script, note that it does all Ansible templating at the top, and then passes them on to the long script. Depending on the intended use, you might name this after the cronjob that will call it, the task it’s going to perform, or whatever makes sense in your environment.

#!/bin/sh
set -eu

# Populated by Ansible
administrative_statement="{{ rolename_administrative_statement }}"
comprehensive_actor="{{ rolename_comprehensive_actor }}"
pleasant_student="{{ rolename_pleasant_student }}"
emotional_president="{{ rolename_emotional_president }}"
numerous_army="{{ rolename_numerous_army }}"
recent_platform="{{ rolename_recent_platform }}"
competitive_tooth="{{ rolename_competitive_tooth }}"
obvious_oven="{{ rolename_obvious_oven }}"
medical_ladder="{{ rolename_medical_ladder }}"
unlikely_tradition="{{ rolename_unlikely_tradition }}"
available_queen="{{ rolename_available_queen }}"
afraid_county="{{ rolename_afraid_county }}"

/usr/local/bin/longscript.py \
    --administrative-statement="$administrative_statement" \
    --comprehensive-actor "$comprehensive_actor" \
    --pleasant-student "$pleasant_student" \
    --emotional-president "$emotional_president" \
    --numerous-army "$numerous_army" \
    --recent-platform "$recent_platform" \
    --competitive-tooth "$competitive_tooth" \
    --obvious-oven "$obvious_oven" \
    --medical-ladder "$medical_ladder" \
    --unlikely-tradition "$unlikely_tradition" \
    --available-queen "$available_queen" \
    --afraid-county "$afraid_county"