vendredi 13 mars 2015

Gestion de l'option changed_when dans Ansible

Petite astuce rapide. Sous Ansible, j'ai voulu supprimer le contenu d'un répertoire (suite à la décompression d'une archive) et je voulais faire le ménage dedans sauf une certaine liste de fichier.

Pour se faire, je passe pas une commande shell et un register pour pouvoir ensuite itérer sur ces fichiers.

Le playbook

Ci-dessous mon playbook :

- name: "Récupération fichier à supprimer du répertoire"
  shell: ls -1 /tmp/emplacement/*.xml | grep -v un-fichier.xml | cat
  register: files_to_remove

- name: "Ménage dans le répertoire."
  file: name={{item}} state=absent
  with_items: files_to_remove.stdout_lines

Seul petit problème, la tache Récupération fichier à supprimer du répertoire m'indique tout le temps qu'il y a eu un changement alors qu'en réalité non.

Ci-dessous un exemple pour illustrer ça :

TASK: [task | Récupération fichier à supprimer du répertoire] **
changed: [localhost]

TASK: [task | Ménage dans le répertoire.] ********************** 
ok: [localhost]

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

Bloquer ce faux changement

Dans ce cas, il est intéressant d'utiliser l'option changed_when avec une condition afin de dire à Ansible que ma commande n'a rien modifiée (ici, ça sera False systématiquement).

Si on l'applique à notre cas, notre main.yml ressemble maintenant à la chose suivante :

- name: "Récupération fichier à supprimer du répertoire"
  shell: ls -1 /tmp/emplacement/*.xml | grep -v un-fichier.xml | cat
  register: files_to_remove
  changed_when: False

- name: "Ménage dans le répertoire."
  file: name={{item}} state=absent
  with_items: files_to_remove.stdout_lines

Dorénavant mes appels ne marquent plus cette tâche comme ayant changée quelque chose.

TASK: [task | Récupération fichier à supprimer du répertoire] ** 
ok: [localhost]

TASK: [task | Ménage dans le répertoire.] ********************** 
ok: [localhost]

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

mardi 10 mars 2015

Activation du cache Ansible par fichier JSON

Lorsque vous lancez Ansible, vous avez déjà dû remarquer que le démarrage peut prendre un certain temps : il s'agit de la phase de récupération des informations sur la machine. Ceci se manifeste au début du lancement d'un playbook sous la forme suivante :

GATHERING FACTS ******************************************* 
ok: [localhost]

Pendant cette phase, Ansible va collecter tout un tas d'indicateur qui vont caractériser votre machine. On va y retrouver pêle-mêle le type de processeur, les disques de la machine, les adresses IP etc. Seul petit problème cette phase prend quelques secondes (voir plus certaines fois) et peut devenir vite agaçante dans le cas où vous mettez au point vos procédures d'installation.

Heureusement, les petits gars d'Ansible ont introduit un petit mécanisme de cache de ces éléments (des facts dans le jargon Ansiblien). Vous pouvez effectivement stocker ça dans un serveur redis.

Comme je n'ai pas trop l'habitude d'utiliser redis et que je n'avais pas envie de passer du temps à faire l'installation de ce nouvel élément sur mon poste perso, je me suis posé la question s'il ne serait pas possible d'utiliser quelque chose de plus simple ... Et la réponse est oui, avec des fichiers au format JSON.

Activation du cache via fichier JSON

Ouvrons notre fichier /etc/ansible/ansible.cfg et allons modifier la valeur du paramètre gathering pour le passer à la valeur smart :

# smart - gather by default, but don't regather if already gathered
# implicit - gather by default, turn off with gather_facts: False
# explicit - do not gather by default, must say gather_facts: True
gathering = smart

Si vous lancez maintenant Ansible, vous ne verrez pas trop de différence. Ce paramètre peut néanmoins être avantageux si vous lancez plusieurs rôles sur les mêmes machines. Dans ce cas, Ansible ne récupérera les caractéristiques de vos machines qu'une seule fois.

Allons un peu plus loin et modifions maintenant les valeurs de fact_caching :

# if set to a persistant type (not 'memory', for example 'redis') fact values
# from previous runs in Ansible will be stored.  This may be useful when
# wanting to use, for example, IP information from one group of servers
# without having to talk to them in the same playbook run to get their
# current IP information.
# fact_caching = memory
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible

Nous avons activé le stockage des fichiers d'inventaire dans le répertoire /tmp/ansible. Chaque serveur verra ses caractéristiques stockées au format JSON.

A noter l'existence d'un paramètre fact_caching_timeout qui vous permettra de contrôler la fraîcheur de vos inventaires.

Différence de vitesse d'exécution dû au cache

Voyons maintenant l'impact des performances avec le test tout simple suivant :

---
# Playbook de test

- name: "Test"
  hosts: "localhost"
  tasks:
    - debug: msg=Lancement réussi

On ne fait qu'afficher un message. Ansible procédera néanmoins à la phase d'inventaire ce qui nous permettra de mesurer ce temps.

Premier lancement

Lançons une première fois notre playbook à froid :

$ time ansible-playbook test.yml 

PLAY [Test] ************************************************* 

GATHERING FACTS ********************************************* 
ok: [localhost]

TASK: [debug msg=Lancement réussi] ************************** 
ok: [localhost] => {
    "msg": "Lancement"
}

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


real    0m2.300s
user    0m1.531s
sys     0m0.708s

Un coup d'oeil dans le répertoire /tmp/ansible :

$ ls /tmp/ansible/
localhost

Ansible a créé un fichier de cache. Un coup d'oeil nous permet de voir qu'il s'agit d'un fichier au format JSON.

Lancement avec cache

Maintenant que nous disposons d'un cache, mesurons à nouveau ce temps d'exécution :

$ time ansible-playbook test.yml 

PLAY [Test] ************************************************* 

TASK: [debug msg=Lancement réussi] ************************** 
ok: [localhost] => {
    "msg": "Lancement"
}

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


real    0m0.303s
user    0m0.242s
sys     0m0.044s

Nous venons de passer de 2,3 secondes à 0,3 seconde soit un gain de 2 secondes.

Pour conclure

Comme nous l'avons vu, le gain n'est pas négligeable (surtout en phase de mise au point) et surtout introduit une notion de persistance de l'information d'une exécution à l'autre. En effet, vous pouvez maintenant récupérer une information venant d'un autre serveur sans pour autant avoir à vous y connecter à chaque fois.

mardi 3 mars 2015

Écriture de module avec Ansible

Depuis quelques temps, je m'étais mis en tête de vouloir tester l'espace disque disponible. En effet, il m'arrive assez régulièrement d'avoir un bon gros message d'erreur plus d'espace disponible alors que je viens de faire la moitié de l'installation.

Partant de ce constat, je me suis dit qu'il vaudrait mieux avoir une erreur franche en début d'exécution plutôt qu'au milieu de mon installation (sachant que généralement je peux avoir un truc à moitié bancal que je dois supprimé pour avoir une installation correcte). J'ai donc cherché un moyen de tester la quantité de disque restant pour un emplacement donné évitant ainsi ce type de désagrément.

Petit problème, je n'ai rien trouvé me permettant de faire ce test simplement. N'étant pas non plus emballé de faire ça avec un bout de shell, j'ai donc cherché à écrire le module Ansible qui me permettrait de gérer ce test.

Fichier playbook

Prenons un playbook check_tmp.yml qui va s'assurer que j'ai au moins 40 Mo d'espace disque dans /tmp. Pour se faire, il va faire appel à un module space_available :

---
# Playbook de test

- name: "Test"
  hosts: "localhost"
  gather_facts: no
  tasks:
    - space_available: path=/tmp size=40M

Fichier du module (library/space_available)

Dans le même répertoire que le fichier check_tmp.yml, créer un répertoire library. Dans ce répertoire, il nous suffit de créer un fichier space_available que nous allons alimenter avec le contenu suivant :

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Author: Yannig Perre 
#
# Check for available space on a given path.
#
 
# Documentation
DOCUMENTATION = '''
---
version_added: "1.8"
module: space_available
short_description: space_available
description:
  - This module check if there's enough space available
options:
  path:
    description:
      path to check
  size:
    description:
      size needed.
notes:
requirements: []
author: Yannig Perre
'''
 
EXAMPLES = '''
- name: "40M space available on /tmp"
  space_available: path=/tmp size=40M
'''
 
# static hash map converter
size_convertion = { "k": 1024, "m": 1024*1024, "g": 1024*1024*1024, "t": 1024*1024*1024*1024, "p": 1024*1024*1024*1024*1024 }

# Here we go...
def main():
    # module declaration
    module = AnsibleModule(
        argument_spec=dict(
            path=dict(required=True),
            size=dict(required=True),
        ),
        supports_check_mode=True
    )
    # Retrieving parameters value
    path = module.params['path']
    size = module.params['size']
    # Retrieve size (code part stolen from lvol module)
    if size[-1].isalpha():
        if size[-1].lower() in 'kmgtpe':
            size_unit = size[-1]
            if not size[0:-1].isdigit():
                module.fail_json(msg="Bad size specification for unit %s" % size_unit)
            size_opt = size[-1]
            size = int(size[0:-1])
    elif not size.isdigit():
        module.fail_json(msg="Bad size specification")
    else:
        size = int(size)
        size_opt = 'm'

    # Convert everything in bytes please
    unit_size = size_convertion[size_opt.lower()]
    size = size * unit_size
    # Try to get ...
    try:
        # ... size available
        statvfs = os.statvfs(path)
    except OSError, e:
        # if we fail dying with exception message
        module.fail_json(msg="%s" % e)
    space_available = statvfs.f_bavail * statvfs.f_frsize

    # If not enough room => fail
    if space_available < size:
        module.fail_json(msg="Not enough space avaible on %s (found = %0.2f%s, needed = %0.2f%s)" % (path, space_available / unit_size, size_opt, size / unit_size, size_opt))
    module.exit_json(msg=path)
 
# Import Ansible Utilities
from ansible.module_utils.basic import *
main()

Premier test

Lançons maintenant notre playbook avec la commande suivante :
ansible-playbook space_available.yml 

PLAY [Test] ***************************************************** 

TASK: [space_available path=/tmp size=40M] ********************** 
ok: [localhost]

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

\o/ Youpi ! Il semblerait que mon poste dispose de plus de 40 Mo de disque dans /tmp !

Quelques tests supplémentaires

Changeons maintenant notre playbook pour voir le comportement de notre module :

---
# Playbook de test

- name: "Test"
  hosts: "localhost"
  gather_facts: no
  tags: "poste-packaging"
  tasks:
    - space_available: path=/does/not/exist size=400M
      ignore_errors: yes
    - space_available: path=/root/.aptitude size=400M
      ignore_errors: yes
    - space_available: path=/tmp size=400M
    - space_available: path=/tmp size=1G
    - space_available: path=/tmp size=37G
      ignore_errors: yes
    - space_available: path=/tmp size=40M
    - space_available: path=/ size=3000M

Lançons notre playbook :

ansible-playbook space_available.yml 

PLAY [Test] **************************************************** 

TASK: [space_available path=/does/not/exist size=400M] ********* 
failed: [localhost] => {"failed": true}
msg: [Errno 2] No such file or directory: '/does/not/exist'
...ignoring

TASK: [space_available path=/root/.aptitude size=400M] ********* 
failed: [localhost] => {"failed": true}
msg: [Errno 13] Permission denied: '/root/.aptitude'
...ignoring

TASK: [space_available path=/tmp size=400M] ******************** 
ok: [localhost]

TASK: [space_available path=/tmp size=1G] ********************** 
ok: [localhost]

TASK: [space_available path=/tmp size=37G] ********************* 
failed: [localhost] => {"failed": true}
msg: Not enough space avaible on /tmp (found = 9.00G, needed = 37.00G)
...ignoring

TASK: [space_available path=/tmp size=40M] ********************* 
ok: [localhost]

TASK: [space_available path=/ size=3000M] ********************** 
ok: [localhost]

PLAY RECAP ***************************************************** 
localhost       : ok=7    changed=0    unreachable=0    failed=0   

Nous avons bien des indications sur :

  • L'absence d'un fichier sur la machine ;
  • Un manque de permission ;
  • Et enfin, un manque de place.
A noter que les instructions en erreur sont passées du fait de la présence du mot clé ignore_errors: yes.

Pour aller plus loin

Pour plus de détail sur le fonctionnement des modules, je vous invite à vous reporter à la documentation sur le sujet présente sur ansible.com (http://docs.ansible.com/developing_modules.html).