Ansible Modules

From Scratch

By Xavi Soler
@xavi_xsb

https://github.com/xsb/ansible-modules-talk

All code used in this presentation is at:

Ansible Modules

Ansible modules are reusable units of magic that can be used by the Ansible API, or by the ansible or ansible-playbook programs.

Ansible Modules   =   Units of Magic

Using the CLI

$ ansible webservers -m apt -a "name=nginx state=present"

Using Playbooks

- name: install nginx
  apt: name=nginx state=present

What Modules Do?

Receive arguments as a filepath

/home/user/.ansible/tmp/ansible-tmp-1452255538.77-268151667579996/arguments

Do something

Usually idempotent actions to ensure state

Return JSON

ie: {"changed":"true"}, other additional info to be used as variables later in the playbook

Modules Development

  • Ansible modules can be developed in any programming language
  • All modules available in modules-core and modules-extras are developed in Python
  • from ansible.module_utils.basic import *

Ansible Library

  • $ANSIBLE_LIBRARY is the environment variable that defines the paths where Ansible modules can be found
  • The /library/ directory in the root of your playbook will be checked as well and it's a good place for project-specific modules

Our First Module

$ find .
.
./playbook.yml
./library
./library/timesync
#!/bin/sh
# sync time using ntp
diff=$(ntpd -q | tail -n 1 | cut -d' ' -f4)
echo -n "{\"changed\":\"true\",\"diff\":\"$diff\"}"
---
- hosts: 127.0.0.1
  sudo: True
  tasks:
    - timesync:

a shell script!

Our First Module

a shell script!

$ ansible-playbook playbook.yml -v

PLAY [127.0.0.1] ************************************************************** 

GATHERING FACTS *************************************************************** 
ok: [127.0.0.1]

TASK: [timesync ] ************************************************************* 
changed: [127.0.0.1] => {"changed": "true", "diff": "+0.000726s"}

PLAY RECAP ******************************************************************** 
127.0.0.1                  : ok=2    changed=1    unreachable=0    failed=0

Our Second Module

do nothing + print input parameters!

$ find .
.
./playbook.yml
./library
./library/print_params
#!/bin/sh
# outputs its own input params
jq -R 'split(" ") | map(split("=") | {key: .[0], value: .[1]}) | from_entries' $1
---
- hosts: 127.0.0.1
  tasks:
    - print_params: name=xavi twitter=@xavi_xsb github=xsb

Our Second Module

do nothing + print input parameters!

$ ansible-playbook playbook.yml -v

PLAY [127.0.0.1] **************************************************************

GATHERING FACTS ***************************************************************
ok: [127.0.0.1]

TASK: [print_params name=xavi twitter=@xavi_xsb github=xsb] *******************
ok: [127.0.0.1] => {"github": "xsb", "name": "xavi", "twitter": "@xavi_xsb"}

PLAY RECAP ********************************************************************
127.0.0.1                  : ok=2    changed=0    unreachable=0    failed=0

Modules in Python

We will implement a simple module:

1) Uploads a file to GitHub Gist

2) Return URL

TASK: [gist src=hello_ansible.md] ********************************************* 
changed: [127.0.0.1] => {"changed": true,
                         "filename": "hello_ansible.md",
                         "url": "https://gist.github.com/6d67067f049fbcf443a4"}
#!/usr/bin/python

import urllib2
import json

def copy_to_gist(public, filename, content):
    data = {"public":public,'files':{filename:{'content':content}}}

    req = urllib2.Request('https://api.github.com/gists')
    req.add_header('Content-Type', 'application/json')
    response = urllib2.urlopen(req, json.dumps(data))
    return response

(...)
(...)

def main():

    module = AnsibleModule(
        argument_spec = dict(
            src    = dict(required=True, type='str'),
            public = dict(required=False, default='false', 
                          choices=['true', 'false']),
        ),
        supports_check_mode = False
    )
    src = module.params['src']
    filename = os.path.basename(src)
    public = module.params['public']
    f = open(os.path.expanduser(src))
    content = f.read()

    try:
        response = copy_to_gist(public, filename, content)
    except Exception as e:
        module.fail_json(msg=str(e), changed=False)

    j = json.load(response)
    module.exit_json(changed=True, filename=filename, url=j['html_url'])

(...)
(...)

# import module snippets
from ansible.module_utils.basic import *

if __name__ == '__main__':
    main()

Demo

Testing Modules

$ git clone git://github.com/ansible/ansible.git --recursive
$ source ansible/hacking/env-setup
$ chmod +x ansible/hacking/test-module
$ ansible/hacking/test-module -m ./mymodule.py -a "key=value"

Conventions

Documentation

DOCUMENTATION = '''
---
module: gist
author: "Xavi S.B. @xsb"
short_description: create Github Gists
description:
  - Copy files to Github Gist
options:
  src:
    description:
      - File Path
    required: true
  public:
    description:
      - Is this Gist public or not?
    required: false
    default: false
    choices: [ "true", "false" ]
'''

Documentation

EXAMPLES = '''
# Upload a config file
- gist: src=/etc/pacman.conf public=false
'''
RETURN = '''
url:
  description: gist url
  returned: success
  type: string
  sample: https://gist.github.com/anonymous/4124f3e2d5441c141796
'''

Questions?

Made with Slides.com