403 lines
No EOL
23 KiB
HTML
403 lines
No EOL
23 KiB
HTML
|
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
|
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
|
|
<title>Module Development Guide — Ansible - SSH-Based Configuration Management & Deployment</title>
|
|
<link rel="stylesheet" href="_static/default.css" type="text/css" />
|
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
|
<link rel="stylesheet" href="_static/bootstrap.css" type="text/css" />
|
|
<link rel="stylesheet" href="_static/bootstrap-sphinx.css" type="text/css" />
|
|
<script type="text/javascript">
|
|
var DOCUMENTATION_OPTIONS = {
|
|
URL_ROOT: '',
|
|
VERSION: '0.01',
|
|
COLLAPSE_INDEX: false,
|
|
FILE_SUFFIX: '.html',
|
|
HAS_SOURCE: false
|
|
};
|
|
</script>
|
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
|
<script type="text/javascript" src="_static/underscore.js"></script>
|
|
<script type="text/javascript" src="_static/doctools.js"></script>
|
|
<script type="text/javascript" src="_static/bootstrap-dropdown.js"></script>
|
|
<script type="text/javascript" src="_static/bootstrap-scrollspy.js"></script>
|
|
<link rel="top" title="Ansible - SSH-Based Configuration Management & Deployment" href="index.html" />
|
|
<link rel="next" title="Frequently Asked Questions" href="faq.html" />
|
|
<link rel="prev" title="API & Integrations" href="api.html" />
|
|
<script type="text/javascript">
|
|
(function () {
|
|
/**
|
|
* Patch TOC list.
|
|
*
|
|
* Will mutate the underlying span to have a correct ul for nav.
|
|
*
|
|
* @param $span: Span containing nested UL's to mutate.
|
|
* @param minLevel: Starting level for nested lists. (1: global, 2: local).
|
|
*/
|
|
var patchToc = function ($span, minLevel) {
|
|
var $tocList = $("<ul/>").attr('class', "dropdown-menu"),
|
|
findA;
|
|
|
|
// Find all a "internal" tags, traversing recursively.
|
|
findA = function ($elem, level) {
|
|
var level = level || 0,
|
|
$items = $elem.find("> li > a.internal, > ul, > li > ul");
|
|
|
|
// Iterate everything in order.
|
|
$items.each(function (index, item) {
|
|
var $item = $(item),
|
|
tag = item.tagName.toLowerCase(),
|
|
pad = 10 + ((level - minLevel) * 10);
|
|
|
|
if (tag === 'a' && level >= minLevel) {
|
|
// Add to existing padding.
|
|
$item.css('padding-left', pad + "px");
|
|
// Add list element.
|
|
$tocList.append($("<li/>").append($item));
|
|
} else if (tag === 'ul') {
|
|
// Recurse.
|
|
findA($item, level + 1);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Start construction and return.
|
|
findA($span);
|
|
|
|
// Wipe out old list and patch in new one.
|
|
return $span.empty("ul").append($tocList);
|
|
};
|
|
|
|
$(document).ready(function () {
|
|
// Patch the global and local TOC's to be bootstrap-compliant.
|
|
patchToc($("span.globaltoc"), 1);
|
|
patchToc($("span.localtoc"), 2);
|
|
|
|
// Activate.
|
|
$('#topbar').dropdown();
|
|
});
|
|
}());
|
|
</script>
|
|
<script type="text/javascript">
|
|
|
|
var _gaq = _gaq || [];
|
|
_gaq.push(['_setAccount', 'UA-29861888-1']);
|
|
_gaq.push(['_trackPageview']);
|
|
|
|
(function() {
|
|
var ga = document.createElement('script'); ga.type =
|
|
'text/javascript'; ga.async = true;
|
|
ga.src = ('https:' == document.location.protocol ? 'https://ssl' :
|
|
'http://www') + '.google-analytics.com/ga.js';
|
|
var s = document.getElementsByTagName('script')[0];
|
|
s.parentNode.insertBefore(ga, s);
|
|
})();
|
|
|
|
</script>
|
|
|
|
</head>
|
|
<body>
|
|
<div class="topbar" data-scrollspy="scrollspy" >
|
|
<div class="topbar-inner">
|
|
<div class="container">
|
|
<a class="brand" href="index.html">Ansible</a>
|
|
<ul class="nav">
|
|
|
|
<li class="dropdown" data-dropdown="dropdown">
|
|
<a href="index.html"
|
|
class="dropdown-toggle">Chapter</a>
|
|
<span class="globaltoc"><ul class="current">
|
|
<li class="toctree-l1"><a class="reference internal" href="gettingstarted.html">Downloads & Getting Started</a></li>
|
|
<li class="toctree-l1"><a class="reference internal" href="patterns.html">The Inventory File, Patterns, and Groups</a></li>
|
|
<li class="toctree-l1"><a class="reference internal" href="examples.html">Command Line Examples</a></li>
|
|
<li class="toctree-l1"><a class="reference internal" href="modules.html">Ansible Modules</a></li>
|
|
<li class="toctree-l1"><a class="reference internal" href="YAMLSyntax.html">YAML Syntax</a></li>
|
|
<li class="toctree-l1"><a class="reference internal" href="playbooks.html">Playbooks</a></li>
|
|
<li class="toctree-l1"><a class="reference internal" href="api.html">API & Integrations</a></li>
|
|
<li class="toctree-l1 current"><a class="current reference internal" href="">Module Development Guide</a></li>
|
|
<li class="toctree-l1"><a class="reference internal" href="faq.html">Frequently Asked Questions</a></li>
|
|
<li class="toctree-l1"><a class="reference internal" href="man.html">Man Pages</a></li>
|
|
</ul>
|
|
</span>
|
|
</li>
|
|
<li class="dropdown" data-dropdown="dropdown">
|
|
<a href="#"
|
|
class="dropdown-toggle">Page</a>
|
|
<span class="localtoc"><ul>
|
|
<li><a class="reference internal" href="#">Module Development Guide</a><ul>
|
|
<li><a class="reference internal" href="#tutorial">Tutorial</a></li>
|
|
<li><a class="reference internal" href="#testing-modules">Testing Modules</a></li>
|
|
<li><a class="reference internal" href="#reading-input">Reading Input</a></li>
|
|
<li><a class="reference internal" href="#common-pitfalls">Common Pitfalls</a></li>
|
|
<li><a class="reference internal" href="#conventions">Conventions</a></li>
|
|
<li><a class="reference internal" href="#shorthand-vs-json">Shorthand Vs JSON</a></li>
|
|
<li><a class="reference internal" href="#sharing-your-module">Sharing Your Module</a></li>
|
|
<li><a class="reference internal" href="#getting-your-module-into-core">Getting Your Module Into Core</a></li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</span>
|
|
</li>
|
|
|
|
</ul>
|
|
<ul class="nav secondary-nav">
|
|
|
|
|
|
<form class="pull-left" action="search.html" method="get">
|
|
<input type="text" name="q" placeholder="Search" />
|
|
<input type="hidden" name="check_keywords" value="yes" />
|
|
<input type="hidden" name="area" value="default" />
|
|
</form>
|
|
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="container">
|
|
|
|
<div class="section" id="module-development-guide">
|
|
<h1>Module Development Guide<a class="headerlink" href="#module-development-guide" title="Permalink to this headline">¶</a></h1>
|
|
<p>Ansible modules are reusable units of magic that can be used by the Ansible API,
|
|
or by the <cite>ansible</cite> or <cite>ansible-playbook</cite> programs.</p>
|
|
<p>Modules can be written in any language and are found in the path specified
|
|
by <cite>ANSIBLE_LIBRARY_PATH</cite> or the <cite>–module-path</cite> command line option.</p>
|
|
<div class="section" id="tutorial">
|
|
<h2>Tutorial<a class="headerlink" href="#tutorial" title="Permalink to this headline">¶</a></h2>
|
|
<p>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.</p>
|
|
<p>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.</p>
|
|
<p>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 <cite>service</cite> or <cite>yum</cite>, and don’t stare too close into things like <cite>async_wrapper</cite> or
|
|
you’ll turn to stone. Nobody ever executes async_wrapper directly.</p>
|
|
<p>Ok, let’s get going with an example. We’ll use Python. For starters, save this as a file named <cite>time</cite>:</p>
|
|
<div class="highlight-python"><div class="highlight"><pre><span class="c">#!/usr/bin/python</span>
|
|
|
|
<span class="kn">import</span> <span class="nn">datetime</span>
|
|
<span class="kn">import</span> <span class="nn">json</span>
|
|
|
|
<span class="n">date</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">())</span>
|
|
<span class="k">print</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span>
|
|
<span class="s">"time"</span> <span class="p">:</span> <span class="n">date</span>
|
|
<span class="p">})</span>
|
|
</pre></div>
|
|
</div>
|
|
</div>
|
|
<div class="section" id="testing-modules">
|
|
<h2>Testing Modules<a class="headerlink" href="#testing-modules" title="Permalink to this headline">¶</a></h2>
|
|
<p>There’s a useful test script in the source checkout for ansible:</p>
|
|
<div class="highlight-python"><pre>git clone git@github.com:ansible/ansible.git
|
|
chmod +x ansible/hacking/test-module</pre>
|
|
</div>
|
|
<p>Let’s run the script you just wrote with that:</p>
|
|
<div class="highlight-python"><pre>ansible/hacking/test-module ./time</pre>
|
|
</div>
|
|
<p>You should see output that looks something like this:</p>
|
|
<div class="highlight-python"><div class="highlight"><pre><span class="p">{</span><span class="s">u'time'</span><span class="p">:</span> <span class="s">u'2012-03-14 22:13:48.539183'</span><span class="p">}</span>
|
|
</pre></div>
|
|
</div>
|
|
<p>If you did not, you might have a typo in your module, so recheck it and try again</p>
|
|
</div>
|
|
<div class="section" id="reading-input">
|
|
<h2>Reading Input<a class="headerlink" href="#reading-input" title="Permalink to this headline">¶</a></h2>
|
|
<p>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 <cite>time=<string></cite> is passed in to the module.</p>
|
|
<p>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.</p>
|
|
<p>The example usage we are trying to achieve to set the time is:</p>
|
|
<div class="highlight-python"><pre>time time="March 14 22:10"</pre>
|
|
</div>
|
|
<p>If no time parameter is set, we’ll just leave the time as is and return the current time.</p>
|
|
<p>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:</p>
|
|
<div class="highlight-python"><div class="highlight"><pre><span class="c">#!/usr/bin/python</span>
|
|
|
|
<span class="c"># import some python modules that we'll use. These are all</span>
|
|
<span class="c"># available in Python's core</span>
|
|
|
|
<span class="kn">import</span> <span class="nn">datetime</span>
|
|
<span class="kn">import</span> <span class="nn">sys</span>
|
|
<span class="kn">import</span> <span class="nn">json</span>
|
|
<span class="kn">import</span> <span class="nn">os</span>
|
|
<span class="kn">import</span> <span class="nn">shlex</span>
|
|
|
|
<span class="c"># read the argument string from the arguments file</span>
|
|
<span class="n">args_file</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
|
|
<span class="n">args_data</span> <span class="o">=</span> <span class="nb">file</span><span class="p">(</span><span class="n">args_file</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
|
|
|
|
<span class="c"># for this module, we're going to do key=value style arguments</span>
|
|
<span class="c"># this is up to each module to decide what it wants, but all</span>
|
|
<span class="c"># core modules besides 'command' and 'shell' take key=value</span>
|
|
<span class="c"># so this is highly recommended</span>
|
|
|
|
<span class="n">arguments</span> <span class="o">=</span> <span class="n">shlex</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">args_data</span><span class="p">)</span>
|
|
<span class="k">for</span> <span class="n">arg</span> <span class="ow">in</span> <span class="n">arguments</span><span class="p">:</span>
|
|
|
|
<span class="c"># ignore any arguments without an equals in it</span>
|
|
<span class="k">if</span> <span class="n">arg</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s">"="</span><span class="p">)</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="p">:</span>
|
|
|
|
<span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="o">=</span> <span class="n">arg</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s">"="</span><span class="p">)</span>
|
|
|
|
<span class="c"># if setting the time, the key 'time'</span>
|
|
<span class="c"># will contain the value we want to set the time to</span>
|
|
|
|
<span class="k">if</span> <span class="n">key</span> <span class="o">==</span> <span class="s">"time"</span><span class="p">:</span>
|
|
|
|
<span class="c"># now we'll affect the change. Many modules</span>
|
|
<span class="c"># will strive to be 'idempotent', meaning they</span>
|
|
<span class="c"># will only make changes when the desired state</span>
|
|
<span class="c"># expressed to the module does not match</span>
|
|
<span class="c"># the current state. Look at 'service'</span>
|
|
<span class="c"># or 'yum' in the main git tree for an example</span>
|
|
<span class="c"># of how that might look.</span>
|
|
|
|
<span class="n">rc</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">system</span><span class="p">(</span><span class="s">"date -s </span><span class="se">\"</span><span class="si">%s</span><span class="se">\"</span><span class="s">"</span> <span class="o">%</span> <span class="n">value</span><span class="p">)</span>
|
|
|
|
<span class="c"># always handle all possible errors</span>
|
|
<span class="c">#</span>
|
|
<span class="c"># when returning a failure, include 'failed'</span>
|
|
<span class="c"># in the return data, and explain the failure</span>
|
|
<span class="c"># in 'msg'. Both of these conventions are</span>
|
|
<span class="c"># required however additional keys and values</span>
|
|
<span class="c"># can be added.</span>
|
|
|
|
<span class="k">if</span> <span class="n">rc</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
|
|
<span class="k">print</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span>
|
|
<span class="s">"failed"</span> <span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
|
|
<span class="s">"msg"</span> <span class="p">:</span> <span class="s">"failed setting the time"</span>
|
|
<span class="p">})</span>
|
|
<span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
|
|
|
|
<span class="c"># when things do not fail, we do not</span>
|
|
<span class="c"># have any restrictions on what kinds of</span>
|
|
<span class="c"># data are returned, but it's always a</span>
|
|
<span class="c"># good idea to include whether or not</span>
|
|
<span class="c"># a change was made, as that will allow</span>
|
|
<span class="c"># notifiers to be used in playbooks.</span>
|
|
|
|
<span class="n">date</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">())</span>
|
|
<span class="k">print</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span>
|
|
<span class="s">"time"</span> <span class="p">:</span> <span class="n">date</span><span class="p">,</span>
|
|
<span class="s">"changed"</span> <span class="p">:</span> <span class="bp">True</span>
|
|
<span class="p">})</span>
|
|
<span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
|
|
|
|
<span class="c"># if no parameters are sent, the module may or</span>
|
|
<span class="c"># may not error out, this one will just</span>
|
|
<span class="c"># return the time</span>
|
|
|
|
<span class="n">date</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">())</span>
|
|
<span class="k">print</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span>
|
|
<span class="s">"time"</span> <span class="p">:</span> <span class="n">date</span>
|
|
<span class="p">})</span>
|
|
</pre></div>
|
|
</div>
|
|
<p>Let’s test that module:</p>
|
|
<div class="highlight-python"><pre>ansible/hacking/test-module ./time time=\"March 14 12:23\"</pre>
|
|
</div>
|
|
<p>This should return something like:</p>
|
|
<div class="highlight-python"><div class="highlight"><pre><span class="p">{</span><span class="s">"changed"</span><span class="p">:</span> <span class="n">true</span><span class="p">,</span> <span class="s">"time"</span><span class="p">:</span> <span class="s">"2012-03-14 12:23:00.000307"</span><span class="p">}</span>
|
|
</pre></div>
|
|
</div>
|
|
</div>
|
|
<div class="section" id="common-pitfalls">
|
|
<h2>Common Pitfalls<a class="headerlink" href="#common-pitfalls" title="Permalink to this headline">¶</a></h2>
|
|
<p>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.:</p>
|
|
<div class="highlight-python"><div class="highlight"><pre><span class="k">try</span><span class="p">:</span>
|
|
<span class="kn">import</span> <span class="nn">json</span>
|
|
<span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
|
|
<span class="kn">import</span> <span class="nn">simplejson</span> <span class="kn">as</span> <span class="nn">json</span>
|
|
</pre></div>
|
|
</div>
|
|
<p>You should also never do this in a module:</p>
|
|
<div class="highlight-python"><div class="highlight"><pre><span class="k">print</span> <span class="s">"some status message"</span>
|
|
</pre></div>
|
|
</div>
|
|
<p>Because the output is supposed to be valid JSON. Except that’s not quite true,
|
|
but we’ll get to that later.</p>
|
|
</div>
|
|
<div class="section" id="conventions">
|
|
<h2>Conventions<a class="headerlink" href="#conventions" title="Permalink to this headline">¶</a></h2>
|
|
<p>As a reminder from the example code above, here are some basic conventions
|
|
and guidelines:</p>
|
|
<ul class="simple">
|
|
<li>Include a minimum of dependencies if possible. If there are dependencies, document them at the top of the module file</li>
|
|
<li>Modules must be self contained in one file to be auto-transferred by ansible</li>
|
|
<li>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.</li>
|
|
<li>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.</li>
|
|
<li>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.</li>
|
|
<li>Return codes are actually not signficant, but continue on with 0=success and non-zero=failure for reasons of future proofing.</li>
|
|
<li>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.</li>
|
|
</ul>
|
|
</div>
|
|
<div class="section" id="shorthand-vs-json">
|
|
<h2>Shorthand Vs JSON<a class="headerlink" href="#shorthand-vs-json" title="Permalink to this headline">¶</a></h2>
|
|
<p>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
|
|
will know what to do:</p>
|
|
<div class="highlight-python"><pre>somekey=1 somevalue=2 rc=3 favcolor=red</pre>
|
|
</div>
|
|
<p>If you’re writing a module in Python or Ruby or whatever, though, returning
|
|
JSON is probably the simplest way to go.</p>
|
|
</div>
|
|
<div class="section" id="sharing-your-module">
|
|
<h2>Sharing Your Module<a class="headerlink" href="#sharing-your-module" title="Permalink to this headline">¶</a></h2>
|
|
<p>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.</p>
|
|
<p><a class="reference external" href="http://groups.google.com/group/ansible-project">Ansible Mailing List</a></p>
|
|
</div>
|
|
<div class="section" id="getting-your-module-into-core">
|
|
<h2>Getting Your Module Into Core<a class="headerlink" href="#getting-your-module-into-core" title="Permalink to this headline">¶</a></h2>
|
|
<p>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.</p>
|
|
<div class="admonition-see-also admonition seealso">
|
|
<p class="first admonition-title">See also</p>
|
|
<dl class="last docutils">
|
|
<dt><a class="reference internal" href="modules.html"><em>Ansible Modules</em></a></dt>
|
|
<dd>Learn about available modules</dd>
|
|
<dt><a class="reference external" href="https://github.com/ansible/ansible/tree/master/library">Github modules directory</a></dt>
|
|
<dd>Browse source of core modules</dd>
|
|
<dt><a class="reference external" href="http://groups.google.com/group/ansible-project">Mailing List</a></dt>
|
|
<dd>Questions? Help? Ideas? Stop by the list on Google Groups</dd>
|
|
<dt><a class="reference external" href="http://irc.freenode.net">irc.freenode.net</a></dt>
|
|
<dd>#ansible IRC chat channel</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
<footer class="footer">
|
|
<div class="container">
|
|
<p class="pull-right"><a href="#">Back to top</a></p>
|
|
<p>
|
|
© Copyright 2012 Michael DeHaan.<br/>
|
|
Last updated on Mar 31, 2012.<br/>
|
|
Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> 1.0.8.<br/>
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
</body>
|
|
</html> |