Aller au contenu principal

Developper des modules Ansible

· 6 minutes de lecture
Stéphane ROBERT
Consultant DevOps

Parfois, il peut être nécessaire de développer ses propres modules Ansible pour répondre à des besoins spécifiques :

  • Automatiser des procédures complexes comme attaquer des API nécessitant de nombreux appels et/ou manipulant des données complexes.
  • Répondre à l'absence de ce module dont vous avez besoin : exemple copier des données d'un bucket à un autre en une seule étape, ...

C'est ce que nous allons voir dans ce billet :

Écrire ses propres modules Ansible

Pour écrire des modules Ansible, il ne faut connaître que le langage Python. Python de nos jours est un des langages des plus courants et qui gagne toujours en popularité.

Je vais vous expliquer comment développer un module Ansible localement. Pour créer votre module, il suffit de créer un répertoire library et d'y mettre des fichiers python dont les noms sont les noms de vos futurs modules. Par exemple, je veux écrire un module hello_world, je crée un fichier library/hello_world.py.

Par contre, si vous le faites dans le cadre de la création de votre propre collection Ansible, il faudra créer vos fichiers dans le répertoire plugins/modules.

Installations d'Ansible

Pour développer un module Ansible, nous avons besoin d'installer Ansible dans un environnement virtuel python. Cela se fait tout simplement.

python -m venv venv
. venv/bin/activate
pip3 install ansible

Notre premier module Ansible

Voici le code type d'un module Ansible retournant juste une chaine contenant Hello World <name> :

#!/usr/bin/env python3

# Copyright: (c) 2018, Terry Jones <terry.jones@example.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = r'''
---
module: hello_world

short_description: This is my first module

# If this is part of a collection, you need to use semantic versioning,
# i.e. the version is of the form "2.5.0" and not "2.4".
version_added: "1.0.0"

description: This is my longer description explaining my test module.

options:
    name:
        description: It is the name to display.
        required: true
        type: str
# Specify this value according to your collection
# in format of namespace.collection.doc_fragment_name
# extends_documentation_fragment:
#     - my_namespace.my_collection.my_doc_fragment_name

author:
    - Your Name (@yourGitHubHandle)
'''

EXAMPLES = r'''
# Pass name to the module
- name: Hello Steph
  my_namespace.my_collection.hello_world:
    name: Steph
'''

RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
message:
    description: The output message that the test module generates.
    type: str
    returned: always
    sample: 'Hello World Stephane'
'''

from ansible.module_utils.basic import AnsibleModule


def main():
    # define available arguments/parameters a user can pass to the module
    module_args = dict(
        name=dict(type='str', required=True),
    )

    # seed the result dict in the object
    # we primarily care about changed and state
    # changed is if this module effectively modified the target
    # state will include any data that you want your module to pass back
    # for consumption, for example, in a subsequent task
    result = dict(
        changed=False,
        message=''
    )

    # the AnsibleModule object will be our abstraction working with Ansible
    # this includes instantiation, a couple of common attr would be the
    # args/params passed to the execution, as well as if the module
    # supports check mode
    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

    # if the user is working with this module in only check mode we do not
    # want to make any changes to the environment, just return the current
    # state with no modifications
    if module.check_mode:
        module.exit_json(**result)

    # during the execution of the module, if there is an exception or a
    # conditional state that effectively causes a failure, run
    # AnsibleModule.fail_json() to pass in the message and the result
    if module.params['name'] == 'fail me':
        result['message'] = 'Outch !!!'
        module.fail_json(msg='The module Failed', **result)

    # use whatever logic you need to determine whether or not this module
    # made any modifications to your target
    if module.params['name'] != 'fail me':
        result['changed'] = True
    result['message'] = 'Hello World %s' % module.params['name']

    # in the event of a successful module execution, you will want to
    # simple AnsibleModule.exit_json(), passing the key/value results
    module.exit_json(**result)

if __name__ == '__main__':
    main()

Quelques explications :

La documentation de notre module est décrite entre :

DOCUMENTATION = r'''

et :

'''

Pour l'afficher il suffit de lancer la commande ansible-doc.

ansible-doc -M library hello_world

This is my longer description explaining my test module.

ADDED IN: version 1.0.0

OPTIONS (= is mandatory):

= name
        It is the name to display.
        type: str

...

from ansible.module_utils.basic import AnsibleModule permet d'importer la classe module:

On crée ensuite une instance de cette classe via :

module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

Ansible_module prend en argument la définition des paramètres de notre module et le support ou pas du mode check.

    module_args = dict(
        name=dict(type='str', required=True),
    )

Les paramètres peuvent être de type :

  • bool : True ou False
  • str
  • choices : définit une liste de paramètres ['premier', 'second'].

On déclare aussi le dictionnaire du résultat envoyé à la sortie standard :

    result = dict(
        changed=False,
        message=''
    )

En sortie du module, il faut utiliser soit la méthode exit_json en cas de succès, soit fail_json en cas d'échec.

Lancer votre module Ansible

Via python

Voyons comment lancer le module sans écrire de playbook, mais via un fichier d'arguments. Donnez les droits d'exécution à notre module :

chmod +x library/hello_world.py

Maintenant, créez un fichier args.json avec ce contenu :

{
    "ANSIBLE_MODULE_ARGS": {
        "name": "hello"
    }
}

Pour finir lancez la commande suivante pour exécuter notre module :

library/hello_world.py args.json | jq

J'ai ajouté jq pour améliorer la sortie :

{
  "changed": true,
  "original_message": "",
  "message": "Hello World Steph",
  "invocation": {
    "module_args": {
      "name": "Steph"
    }
  }
}

Ce mode de lancement est bien utile pour débugger notre module avec Visual Code. Je vous expliquerai tout cela dans un prochain billet.

Via un playbook

Il suffit de créer notre playbook avec ce contenu :

- name: Test de mon module
  hosts: localhost
  connection: local
  tasks:
  - name: Hello Steph
    hello_world:
      name: 'fail me'
    register: output
  - name: dump test output
    debug:
      var: output.message

Et la classique commande :

ansible-playbook my_playbook.yml

PLAY [Test de mon module] *********************************

TASK [Gathering Facts] ************************************
ok: [localhost]

TASK [Hello Steph] ****************************************
fatal: [localhost]: FAILED! => {"changed": false, "message": "Outch !!!", "msg": "FAILED"}

PLAY RECAP ************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

Ansible retrouve le module car par défaut, il cherche dans le répertoire local la présence d'un dossier library. Si les modules sont ailleurs il faudra créer un fichier ansible.cfg :

[defaults]
library = <path to modules>

Je lui ai donné en paramètre fail me ce qui a entrainé la sortie en erreur.

En espérant que cela aille vous aider à développer vos propres modules Ansible. Personnellement, je le fais, car cela parfois simplifie l'écriture des playbooks et centralise tout le code dans un seul endroit.