Skip to main content

ansible-playbook wrapper with YAML-abstracted python click cli options

Project description

Table of Contents generated with DocToc

Overview

This is a task runner that serves as a higher-level automation layer to ansible

The script expects an ansible-playbook file as the task manifest.

By default, this is a file named 'Taskfile.yaml' in the current working directory.

The inspiration for the tool comes from the gnu make command, which operates in similar fashion, i.e.

  • A Makefile defines available build steps
  • The make command consumes the Makefile at runtime and exposes these steps as command-line options

Jump down to the usage examples to see this in action.

Installation Instructions

Use case and example

Given

  1. An enterprise-grade application named contoso-app
  2. Multiple teams:
  • Development
  • Engineering
  • DBA
  • Operations
  • QA
  1. Ansible is the primary means of invoking business and operational processes across the numerous environment(s)

Task

You must ensure all teams adopt a standardized approach to running ansible workloads

Investigation

Upon investigating the current approach, you observe the following:

  • Users tend to create wrapper scripts that call the ansible-playbook command
  • These scripts don't follow any naming convention, as you've noted:
    • run.sh
    • start.sh
    • playbook.sh
  • These shell scripts have common attributes:
    • Dynamically populate ansible-playbook variables via the --extra-vars option
    • Dynamically creating ansible inventories
    • Performing pre/post-flight tasks
    • Providing a command-line interface

Assessment

Advantages to the above approach:

  • Quick-n-dirty, anyone can get started relatively quickly with writing ansible automation

Disadvantages:

  • Lack of standards:
  • Leads to difficulty in collaboration and code refactoring
  • Decreased re-usability of codebase
    • This design encourages standalone playbooks
    • Makes it more difficult to package actions as roles
    • Duplicate efforts across codebase

Proposed Solution

Create ansible task runner that reads a specially formatted ansible playbook (Taskfile.yaml)

  • Accomplishes the same as the above, but in more uniform manner
  • Each tasks playbook behaves like a command-line script
  • Support for command-line parameters/flags
  • Embedded dynamic inventory
  • Embedded shell functions

Advantages to this approach:

  • Easier to manage
    • If you know YAML and Ansible, you can get started relatively quickly with writing ansible automation
  • Single executable (/usr/local/bin/tasks)

Disadvantages:

  • Target ansible controller needs to have the tasks command installed

Technical Details

As stated in the overview, this tool functions much like the make command in that it accepts an input file that essentially extends its cli options.

We create a specially formatted ansible-playbook that serves as a task definition file (by default, Taskfile.yaml).

In the following sections, we'll be building a sample manifest/playbook named Taskfile.yaml

Add hosts designation

Add hosts, gather_facts, etc

Taskfile.yaml

- hosts: myhosts
  gather_facts: true
  become: true

Add vars key

Remember, the task runner will ultimately be calling the ansible-playbook command against this very same file, so it must be conformant.

We add the 'vars' key, which allows ansible to populate the variables we are defining in this block.

Taskfile.yaml

- hosts: myhosts
  gather_facts: true
  become: true
  vars:

Populate the vars block - defaults

Let's add some default variables to the playbook:

Taskfile.yaml

- hosts: myhosts
  gather_facts: true
  become: true
  vars:
    myvar1: myvalue1
    myvar2: myvalue2
    myvar3: myvalue3
    myvar4: |
      This is a multi-line value
      of type string
    myvar5:
      - mylistvalue1
      - mylistvalue2
      - mylistvalue3
      - mylistvalue4

Populate the vars block - cli options

Next, we add the cli interface:

Taskfile.yaml

- hosts: myhosts
  gather_facts: true
  become: true
  vars:
    myvar1: myvalue1
    myvar2: myvalue2
    myvar3: myvalue3
    myvar4: |
      This is a multi-line value
      of type string
    myvar5:
      - mylistvalue1
      - mylistvalue2
      - mylistvalue3
      - mylistvalue4
    required_parameters:
      -d|--db-hosts: dbhosts
      -w|--web-hosts: webhosts
      -t|--some-parameter: some_value
    optional_parameters:
      -l|--another-parameter: another_value
      -A: hello
      -PR: preflight_and_run
      --debug-mode: debug_mode

Notice the parameter definitions:

  • required_parameters
  • optional_paramters

These are yaml list objects that expose optional and required command-line options.

The syntax for the options is as follows:

Options                                      | Mapped Variable
-------------------------------------------- | ----------------------
-{{ short_option }}|--{{ long_option }}      | {{ mapped_variable }}
-{{ switch }}                                | {{ mapped_variable }} (boolean)
--{{ switch }}                               | {{ mapped_variable }} (boolean)

Essentially, any option whose key contains a pipe '|' character is evaluated as a click option, which means you must provide an argument to said option.

Anything else is treated as a switch, which evaluates to True if specified, and undefined otherwise (unless you provide a default in your vars declaration).

Examples:

Options       | Mapped Variable
------------- | -------------
-f|--foo      | some_foo_variable
-b|--bar      | some_bar_variable
-F|--foo-bar  | some_other_variable
-a|--all-else | [remaining_args] (behaves like click's variadic arguments (nargs=*))
--some-option | some_switch (behaves like click switches, holds the value of True if specified)

More flexibility can be achieved through the use of parameter sets.

See the appendix for more information.

Populate the vars block - cli options - mapped variables

It's important to note that the above mapped variables can be used during runtime, i.e. referenced in any defined functions, embedded inventory logic, etc.

Consider the -f|-foo option above.

Whatever argument you pass to this option becomes the value for the mapped variable.

Again, this variable is made available to the underlying subprocess call, and within the ansible playbook itself.

Populate the vars block - help/message

Next, we add the help/message section

Taskfile.yaml

- hosts: myhosts
  gather_facts: true
  become: true
  vars:
    myvar1: myvalue1
    myvar2: myvalue2
    myvar3: myvalue3
    myvar4: |
      This is a multi-line value
      of type string
    myvar5:
      - mylistvalue1
      - mylistvalue2
      - mylistvalue3
      - mylistvalue4
    required_parameters:
      -d|--db-hosts: dbhosts
      -w|--web-hosts: webhosts
      -t|--some-parameter: some_value
    optional_parameters:
      -l|--another-parameter: another_value
      -A: hello
      -PR: preflight_and_run
      --debug-mode: debug_mode
    help:
      message: |
        Do something against db and web hosts
      epilog: |
        This line will be displayed at the end of the help text message
      examples:
        - example1: |
            Usage example 1
        - example2: |
            Usage example 2

Populate the vars block - inventory

Add the dynamic inventory section

Taskfile.yaml

- hosts: myhosts
  gather_facts: true
  become: true
  vars:
    myvar1: myvalue1
    myvar2: myvalue2
    myvar3: myvalue3
    myvar4: |
      This is a multi-line value
      of type string
    myvar5:
      - mylistvalue1
      - mylistvalue2
      - mylistvalue3
      - mylistvalue4
    required_parameters:
      -d|--db-hosts: dbhosts
      -w|--web-hosts: webhosts
      -t|--some-parameter: some_value
    optional_parameters:
      -l|--another-parameter: another_value
      -A: hello
      -PR: preflight_and_run
      --debug-mode: debug_mode
    help:
      message: |
        Do something against db and web hosts
      epilog: |
        This line will be displayed at the end of the help text message
      examples:
        - example1: |
            Usage example 1
        - example2: |
            Usage example 2
    inventory: |
      [web-hosts]
      $(echo ${webhosts} | tr ',' '\\n')
      [db-hosts]
      $(echo ${dbhosts} | tr ',' '\\n')
      [myhosts:children]
      deployment-hosts
      web-hosts
      db-hosts

Populate the vars block - embedded functions

Add embedded functions:

Taskfile.yaml

- hosts: myhosts
  gather_facts: true
  become: true
  vars:
    myvar1: myvalue1
    myvar2: myvalue2
    myvar3: myvalue3
    myvar4: |
      This is a multi-line value
      of type string
    myvar5:
      - mylistvalue1
      - mylistvalue2
      - mylistvalue3
      - mylistvalue4
    required_parameters:
      -d|--db-hosts: dbhosts
      -w|--web-hosts: webhosts
      -t|--some-parameter: some_value
    optional_parameters:
      -l|--another-parameter: another_value
      -A: hello
      -PR: preflight_and_run
      --debug-mode: debug_mode
    help:
      message: |
        Do something against db and web hosts
      epilog: |
        This line will be displayed at the end of the help text message
      examples:
        - example1: |
            Usage example 1
        - example2: |
            Usage example 2
    inventory: |
      [web-hosts]
      $(echo ${webhosts} | tr ',' '\\n')
      [db-hosts]
      $(echo ${dbhosts} | tr ',' '\\n')
      [myhosts:children]
      deployment-hosts
      web-hosts
      db-hosts
    functions:
      hello:
        shell: bash
        source: |-
          echo hello
      preflight_and_run:
        shell: bash
        source: |-
          echo 'Running Preflight Tasks!'
          tasks run -d dbhost1 -w webhost1 -t value1

Notice the two switches -A and -PR.

These map to the variables hello and preflight_and_run, respectively.

Now, because these mappings have corresponding keys in the embedded functions stanza, specifying the options in your tasks invocation will short-circuit normal operation and execute the corresponding functions in the order you called them.

For usage examples, see the appendix.

Add tasks

Finally, we add tasks!

Taskfile.yaml

- hosts: myhosts
  gather_facts: true
  become: true
  vars:
    myvar1: myvalue1
    myvar2: myvalue2
    myvar3: myvalue3
    myvar4: |
      This is a multi-line value
      of type string
    myvar5:
      - mylistvalue1
      - mylistvalue2
      - mylistvalue3
      - mylistvalue4
    required_parameters:
      -d|--db-hosts: dbhosts
      -w|--web-hosts: webhosts
      -t|--some-parameter: some_value
    optional_parameters:
      -l|--another-parameter: another_value
      -A: hello
      -PR: preflight_and_run
      --debug-mode: debug_mode
    help:
      message: |
        Do something against db and web hosts
      epilog: |
        This line will be displayed at the end of the help text message
      examples:
        - example1: |
            Usage example 1
        - example2: |
            Usage example 2
    inventory: |
      [web-hosts]
      $(echo ${webhosts} | tr ',' '\\n')
      [db-hosts]
      $(echo ${dbhosts} | tr ',' '\\n')
      [myhosts:children]
      deployment-hosts
      web-hosts
      db-hosts
    functions:
      hello:
        shell: bash
        source: |-
          echo hello
  tasks:
    - debug: 
        msg: |
          Hello from Ansible!
          You specified: {{ some_value }}
          You specified: {{ another_value }}

Usage Examples

Quick usage examples:

  • Display help for main command tasks --help
  • Display help for the run subcommand tasks run --help
  • Don't do anything, just echo the underlying shell command tasks run -d dbhost1 -w webhost1 -t value1 --echo Result should be similar to: ansible-playbook -i C:\Users\${USERNAME}\AppData\Local\Temp\ansible-inventory16xdkrjd.tmp.ini -e dbhosts="dbhost1" -e webhosts="webhost1" -e some_value="value1" -e echo="True" Taskfile.yaml
  • Run the playbook tasks run -d dbhost1 -w webhost1 -t value1
  • Run the embedded function preflight_and_run tasks run -d dbhost1 -w webhost1 -t value1 -PR
  • Run the embedded functions hello and preflight_and_run tasks run -d dbhost1 -w webhost1 -t value1 -A -PR

Installation

Ansible-tasksrunner consists of the tasks binary (for now), and it can be installed in a few ways:

More Examples

Review the examples directory for more hands-on usage samples.

Appendix

Special Variables

ansible_playbook_command

If you define the playbook variable ansible_playbook_command, this will override the underlying ansible-playbook command invocation.

As an example, suppose I define this variable in the above Taskfile.yaml, as follows:

- hosts: myhosts
  gather_facts: true
  become: true
  vars:
    ansible_playbook_command: 'python ${HOME}/ansible_2.7.8/ansible-playbook'
    myvar1: myvalue1
    myvar2: myvalue2
    myvar3: myvalue3
    # ...

Upon invoking the tasks command with the --echo flag, the underlying shell command would then be revealed as:

python ${HOME}/ansible_2.7.8/ansible-playbook -i C:\Users\${USERNAME}\AppData\Local\Temp\ansible-inventory16xdkrjd.tmp.ini -e dbhosts="dbhost1" -e webhosts="webhost1" -e some_value="value1" -e echo="True" Taskfile.yaml

parameter_set

What if you wanted to operate under multiple contexts?

e.g. You want to be able to interact with Amazon Web Services (AWS) and Google Cloud Platform (GCP)?

Sure, you could add paramters to your heart's content, but you'll pollute the output from --help

This is where parameter sets come into play.

The functionality is simple. Precede the run subcommand with a single word.

This word acts as a mini subcommand, and unlocks the command-line options defined by the corresponding key in the appropriate options section of your manifest.

Here's an example:

    required_parameters:
      aws:
        -aws|--some-aws-option: aws_option
      gcp:
        -gcp|--some-gcp-option: gcp_option
      -d|--db-hosts: dbhosts
      -w|--web-hosts: webhosts
      -t|--some-parameter: some_value

Note the aws and gcp keys.

You'll notice that the output of --help will change depending on which parameters set you specify, e.g.

tasks aws run --help

tasks gcp run --help

Another thing to note is that the parameter set you specify is tracked during runtime as the variable parameter_set

You can use this behavior to detect when a given parameter set has been activated.

Single-Executable Releases

This script also ships as a zipapp executable (similar to a windows .exe).

Head over to the releases page for release downloads.

You can also build your own single-executable zipapp, as follows:

  1. Make sure you have the make-zipapp executable in your path
  2. Invoking build tasks
  • Build zipapp: ./tasks.py -f build.yaml run -b
  • Build zipapp and push to remote host (via scp): ./tasks.py -f build.yaml run -b -bp someserver.somedomain.local:/home/${USER-USERNAME}

Read More on zipapps: zipapp — Manage executable Python zip archives — Python 3.7.4rc2 documentation

License and Credits

This project adopts the the MIT distribution License.

Releases come bundled with the following opensource python packages:

  • click, licensed under BSD-3-Clause
  • pyYaml, licensed under MIT

Lastly, this package was created with Cookiecutter_ and the audreyr/cookiecutter-pypackage_ project template.

.. _Cookiecutter: https://github.com/audreyr/cookiecutter .. _audreyr/cookiecutter-pypackage: https://github.com/audreyr/cookiecutter-pypackage

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ansible_taskrunner-0.0.18.tar.gz (31.0 kB view hashes)

Uploaded Source

Built Distribution

ansible_taskrunner-0.0.18-py3-none-any.whl (26.9 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page