ansible/rst/moduledev.rst

254 lines
8.8 KiB
ReStructuredText
Raw Normal View History

Module Development Guide
========================
.. seealso::
:doc:`modules`
Learn about available modules
`Github modules directory <https://github.com/ansible/ansible/tree/master/library>`_
Browse source of core modules
Ansible modules are reusable units of magic that can be used by the Ansible API,
or by the `ansible` or `ansible-playbook` programs.
Modules can be written in any language and are found in the path specified
by `ANSIBLE_LIBRARY_PATH` or the `--module-path` command line option.
Tutorial
````````
Let's build a module to get and set the system time. For starters, let's build
a module that just outputs the current time.
We are going to use Python here but any language is possible. Only File I/O and outputing to standard
out are required. So, bash, C++, clojure, Python, Ruby, whatever you want
is fine.
It's obvious that you would never really need to build a module to set the system time,
the 'command' module could already be used to do this. However, it makes for a decent example.
Reading the modules that come with ansible (linked above) is a great way to learn how to write
modules. Keep in mind, though, that some modules in ansible's source tree are internalisms,
so look at `service` or `yum`, and don't stare too close into things like `async_wrapper` or
you'll turn to stone. Nobody ever executes async_wrapper directly.
Ok, let's get going with an example. We'll use Python. For starters, save this as a file named `time`::
#!/usr/bin/python
import datetime
import json
date = str(datetime.datetime.now())
print json.dumps({
"time" : date
})
Testing Modules
```````````````
There's a useful test script in the source checkout for ansible::
git clone git@github.com:ansible/ansible.git
chmod +x ansible/hacking/test-module
Let's run the script you just wrote with that::
ansible/hacking/test-module ./time
You should see output that looks something like this::
{u'time': u'2012-03-14 22:13:48.539183'}
If you did not, you might have a typo in your module, so recheck it and try again
Reading Input
`````````````
Let's modify the module to allow setting the current time. We'll do this by seeing
if a key value pair in the form `time=<string>` is passed in to the module.
Ansible internally saves arguments to a arguments file. So we must read the file
and parse it. The arguments file is just a string, so any form of arguments are legal.
Here we'll do some basic parsing to treat the input as key=value.
The example usage we are trying to achieve to set the time is::
time time="March 14 22:10"
If no time parameter is set, we'll just leave the time as is and return the current time.
.. note:
This is obviously an unrealistic idea for a module. You'd most likely just
use the shell module. However, it probably makes a decent tutorial.
Let's look at the code. Read the comments as we'll explain as we go. Note that this
highly verbose because it's intended as an educational example. You can write modules
a lot shorter than this::
#!/usr/bin/python
# import some python modules that we'll use. These are all
# available in Python's core
import datetime
import sys
import json
import os
import shlex
# read the argument string from the arguments file
args_file = sys.argv[1]
args_data = file(args_file).read()
# for this module, we're going to do key=value style arguments
# this is up to each module to decide what it wants, but all
# core modules besides 'command' and 'shell' take key=value
# so this is highly recommended
arguments = shlex.split(args_data)
for arg in arguments:
# ignore any arguments without an equals in it
if arg.find("=") != -1:
(key, value) = arg.split("=")
# if setting the time, the key 'time'
# will contain the value we want to set the time to
if key == "time":
# now we'll affect the change. Many modules
# will strive to be 'idempotent', meaning they
# will only make changes when the desired state
# expressed to the module does not match
# the current state. Look at 'service'
# or 'yum' in the main git tree for an example
# of how that might look.
rc = os.system("date -s \"%s\"" % value)
# always handle all possible errors
#
# when returning a failure, include 'failed'
# in the return data, and explain the failure
# in 'msg'. Both of these conventions are
# required however additional keys and values
# can be added.
if rc != 0:
print json.dumps({
"failed" : True,
"msg" : "failed setting the time"
})
sys.exit(1)
# when things do not fail, we do not
# have any restrictions on what kinds of
# data are returned, but it's always a
# good idea to include whether or not
# a change was made, as that will allow
# notifiers to be used in playbooks.
date = str(datetime.datetime.now())
print json.dumps({
"time" : date,
"changed" : True
})
sys.exit(0)
# if no parameters are sent, the module may or
# may not error out, this one will just
# return the time
date = str(datetime.datetime.now())
print json.dumps({
"time" : date
})
Let's test that module::
2012-03-14 17:13:32 +00:00
ansible/hacking/test-module ./time time=\"March 14 12:23\"
This should return something like::
{"changed": true, "time": "2012-03-14 12:23:00.000307"}
Common Pitfalls
```````````````
If writing a module in Python and you have managed nodes running
Python 2.4 or lower, this is generally a good idea, because
json isn't in the Python standard library until 2.5.::
try:
import json
except ImportError:
import simplejson as json
You should also never do this in a module::
print "some status message"
Because the output is supposed to be valid JSON. Except that's not quite true,
but we'll get to that later.
Conventions
2012-03-15 03:45:02 +00:00
```````````
As a reminder from the example code above, here are some basic conventions
and guidelines:
* Include a minimum of dependencies if possible. If there are dependencies, document them at the top of the module file
* Modules must be self contained in one file to be auto-transferred by ansible
* If packaging modules in an RPM, they only need to be installed on the control machine and should be dropped into /usr/share/ansible. This is entirely optional.
* Modules should return JSON or key=value results all on one line. JSON is best if you can do JSON. All return types must be hashes (dictionaries) although they can be nested.
* In the event of failure, a key of 'failed' should be included, along with a string explanation in 'msg'. Modules that raise tracebacks (stacktraces) are generally considered 'poor' modules, though Ansible can deal with these returns and will automatically convert anything unparseable into a failed result.
* Return codes are actually not signficant, but continue on with 0=success and non-zero=failure for reasons of future proofing.
* As results from many hosts will be aggregrated at once, modules should return only relevant output. Returning the entire contents of a log file is generally bad form.
Shorthand Vs JSON
2012-03-15 03:45:02 +00:00
`````````````````
To make it easier to write modules in bash and in cases where a JSON
module might not be available, it is acceptable for a module to return
key=value output all on one line, like this. The Ansible parser
2012-03-14 17:13:32 +00:00
will know what to do::
2012-03-14 17:13:32 +00:00
somekey=1 somevalue=2 rc=3 favcolor=red
If you're writing a module in Python or Ruby or whatever, though, returning
JSON is probably the simplest way to go.
Sharing Your Module
```````````````````
If you think your module is generally useful to others, Ansible is preparing
an 'ansible-contrib' repo. Stop by the mailing list and we'll help you to
get your module included. Contrib modules can be implemented in a variety
of languages. Including a README with your module is a good idea so folks
can understand what arguments it takes and so on. We would like to build
up as many of these as possible in as many languages as possible.
`Ansible Mailing List <http://groups.google.com/group/ansible-project>`_
Getting Your Module Into Core
`````````````````````````````
High-quality modules with minimal dependencies
can be included in the core, but core modules (just due to the programming
preferences of the developers) will need to be implemented in Python.
Stop by the mailing list to inquire about requirements.