I started creating and maintaining Ansible modules for Cumulus Networks about a year ago, and love it.

Unit testing Ansible modules is important to me. I realized writing unit tests reduced the time I spent performing integration tests on real switches.

I will show a simple module and discuss how I configured unit tests for it.

Module structure

Ansible modules produce 3 main types of exit signals:

  • AnsibleModule.exit_json(changed=True): This means something changed. This activates the notify action, also if available

  • AnsibleModule.exit_json(changed=False): Nothing changed

  • AnsibleModule.fail_json(msg="Something bad"): This exits the playbook. No further tasks should be run.

In this example, the module uses the following arguments:

  • prefix - can be something like '10.1.1.0/24'

  • timeout - how long to run the check before quitting.

Pseudo code for the module

  1. Create Ansible module instance
  2. Check if route exists.
    • If route exists, return "no change"
    • If route doesn not exist, "Fail"
    • module does not have a "changed" option

It is common for modules to report a change if something happens. This is one of those few examples where reporting a change doesn't make sense.

The common use case for this module is when configuring a routing protocol on the switch or server, you want to confirm that the routes are been exchanged. So the prefix_check module, ensures that routing is working as expected. If the route is missing, then Fail.

Time for some code

Below is the main() function. Notice that main() is split out

module: prefixcheck author: Stanley Kamithi skamithi@gmail.com shortdescription: Check route prefix description: - Inspired by Cumulus Linux prefix_check Ansible Module. \ Given a route prefix, check to see if route exists. \ If the route exists, then do nothing. If route does not exist \ the module will exit with an error. options: prefix: description: - route to check. required: true timeout: description: - timeout interval. if route is not found by the \ time timeout kicks in then exit module '''

handy helper for calling system calls.

calls AnsibleModule.run_command and prints a more appropriate message

exec_path - path to file to execute, with all its arguments.

E.g "/sbin/ip -o link show"

failure_msg - what message to print on failure

def runcmd(module, execpath, failuremsg): (rc, out, err) = module.runcommand(execpath) if rc == 1: module.failjson(msg=failure_msg) return out

def singleroutecheckrun(module): output = runcmd(module, '/sbin/ip route show %s' % (module.prefix), '/sbin/ip failed to execute. Check if prog exists') # if route is found, output will not be blank if len(output.splitlines()) > 0: return True return False

def checkifrouteexists(module): timeout = 0 # Start loop while True: # if route is found, return True if singleroutecheckrun(module): return True # if timeout occurs return False elif timeout == module.timeout: return False # otherwise sleep for poll interval and repeat the test else: timeout += 1 time.sleep(module.poll_interval)

def main(): module = AnsibleModule( argument_spec=dict( prefix=dict(required=True, type='str'), timeout=dict(default=5, type='int'), ), )

# define some hardcoded variables
# polling interval in sec between each ip route show execution
module.poll_interval = 1

# function runs a check.
# If after timeout it will return false
return_val = check_if_route_exists(module)
if return_val is True:
    module.exit_json(changed=False, msg="Route Found")
else:
    module.fail_json(msg="Route not Found. Check Routing Configuration")

import module snippets

from ansible.module_utils.basic import * import time

notice lots of modules don't have this name.

Wonder if its not required?

if name == 'main': main()

What to perform unit testing on?

  1. main()

    • Check AnsibleModule variable inputs. In my experience I fat finger stuff in the main() function all the time. This adds a basic sanity check to ensure I don't make any mistakes with the module arguments. This kind of error is easily detected in live system testing. I feel it saves me time during live testing to have this unit test in place.
    • Module exits correctly under different conditions. That is, if route check fails, run the exit_json() function. If it passes run the fail_json function. When making changes in the main(), I sometimes mess up the exit logic. This makes sure to catch any errors in the basic outcome logic of the module
  2. check_if_route_exists()

    • Mock ip route calls and if the route exists, return true
    • Mock the timeout, and ensure it returns false if the timeout is reached
  3. Test that the ip route call is correct

    • This ensures that future modifications of the module don't accidentally change the important system calls of this module.

The next blog post goes into test details and how I performed mocking.

Unit tests eliminate a lot of the common coding errors, I would otherwise encounter during integration testing on real switches. It has saved me a lot of time, and adds some dependability to modules I work on.