mercredi 25 mai 2016

Ecriture de filtre Ansible

Écriture de filtre avec Ansible

Ansible est un merveilleux outils pour gérer la mise à jour de vos serveurs. Un gros avantage de ce produit et de permettre d'étendre ses capacités assez facilement à l'aide d'un mécanisme de plugin. Vous pourrez retrouver un petit article que j'avais écrit sur la notion de module à l'emplacement suivant : Écriture de module avec ansible.

Aujourd'hui, je vais vous présenter un autre mécanisme Ansible : les filtres.

Pour info, la notion de filtre vient du monde Jinja. Comme ce moteur est au coeur du fonctionnement d'Ansible, il est donc très facile d'utiliser les mêmes mécanismes chez ce dernier.

Dans ce qui va suivre, nous allons voir comment écrire un filtre spécifique et comment l'utiliser au sein d'Ansible.

Écriture du filtre

Avant toute chose, nous allons devoir écrire une fonction python qui prendra un argument (on peut en mettre plusieurs mais il faudra les rajouter à la suite du filtre entre parenthèse). Cet argument va contenir la chaîne en entrée à transformer. Ci-dessous un exemple de fonction permettant de formater la date du jour à l'aide d'une chaîne de caractère (un peu comme le ferait la commande Unix date) :

def strftime(string_format):
    '''
    Renvoie une chaîne formatée par strftime (ex : strftime('%Y') => 2016)
    '''
    return time.strftime(string_format, time.localtime())

Intégration dans Ansible

Rajoutons maintenant le nécessaire pour qu'Ansible puisse utiliser cette fonction :

# -*- coding: utf-8 -*-
# Filtre strftime

from __future__ import absolute_import
from ansible import errors

import time

def strftime(string_format):
    '''
    Renvoie une chaîne formatée par strftime (ex : strftime('%Y') => 2016)
    '''
    return time.strftime(string_format, time.localtime())

class FilterModule(object):
    ''' On renvoie tout ça dans un objet filtre '''

    def filters(self):
        return { 'strftime': strftime }

Prise en compte

Mettez tout ceci dans le fichier strftime.py (par exemple). Il ne nous reste plus qu'à déclarer l'emplacement du chemin des plugins custom dans le fichier /etc/ansible/ansible.cfg (ou sinon dans le fichier .ansible.cfg dans votre répertoire home) :

filter_plugins = /emplacement/de/mes/filtres/custom

Tout est prêt, nous allons pouvoir passer à l'utilisation du filtre.

Et maintenant, les tests !

Lançons maintenant un petit test pour vérifier que tout ceci fonctionne avec la date du jour par exemple :

ansible -m debug -a msg="{{'%Y%m%d'|strftime}}" localhost
localhost | SUCCESS => {
   "msg": "20160525"
}

Rajoutons maintenant l'heure :

ansible -m debug -a msg="{{'%Y%m%d %H:%M'|strftime}}" localhost
localhost | SUCCESS => {
    "msg": "20160525 11:06"
}

Ça semble plutôt bien marcher !

Le petit mot de la fin

Vous pouvez maintenant utiliser ce mécanisme où bon vous semblera (déclaration de variable, contenu des paramètres Ansible). Attention toutefois à ne pas oublier la déclaration du répertoire des filtres.

Voilà, ça sera tout pour aujourd'hui !

lundi 9 mai 2016

Prototypage d'infra à l'aide de Docker et Ansible

J'utilise depuis quelques temps l'outil Ansible pour gérer mes environnements. Il y a peu, j'ai eu besoin de mettre en place des tests automatiques. Comme toujours, c'est posé la question de savoir où lancer ces fameux tests.

La réponse classique aurait de se baser sur des VM mais comme d'habitude, j'avais des difficultés à avoir ces machines :

  • Il faut demander une entrée DNS ;
  • Il me faut une adresse IP ;
  • Il faut l'installer ;
  • Attention, l'hyperviseur est bientôt saturé ;
  • Mais qui va payer ?
  • Mais qui va sauvegarder ?

Bref, comme toujours, la VM de test est presque aussi compliquée à installer qu'une VM de production et je n'arrive pas à expliquer aux gens qu'il s'agit d'un besoin jetable.

Dans ce cadre, j'ai repensé aux quelques tests que j'avais réalisé sur Docker il y a quelques temps et je me suis dit que ce produit pourrait très bien répondre à mon besoin.

Disclaimer : Ce qui va suivre n'est absolument pas conseillé dans le cadre d'une production. Ce besoin s'inscrit dans un besoin de prototypage d'infra. Ce mode de fonctionnement n'est pas assez stable - du moins pour l'instant - dans des besoins sérieux.

Bref, je demande une VM avec Docker dessus et partant de là, je peux commencer à travailler.

Le principe

Mon besoin est de pouvoir me connecter à un container en faisant mes opérations comme si j'étais dans une vraie machine. Pour se faire, je suis quand même obligé de lancer un process pour faire croire à Docker et Ansible que quelque chose tourne. Dans un premier temps, j'ai fait quelques tests en lançant avec des commandes sleep ou tail -f /dev/null. Le problème est que lorsque j'ai voulu démarrer mes services à l'aide de systemd, j'ai eu quelques plantages.

Problème systemd dans Docker

L'un des premiers problèmes que j'ai abordé a été la gestion des spécificités de systemd. En effet, dans le cas des distributions modernes (centos 7, ubuntu 15.10 ou 16.04), le démarrage est entièrement géré par systemd. Le seul petit hic étant que ce dernier change entièrement la façon de gérer les services. C'est assez marrant d'ailleurs, ce changement m'a fait pensé à la refonte des services dans Solaris 10 (attention, un troll c'est glissé dans ce paragraphe).

Il faut donc embarquer systemd dans votre container Docker. Mais d'un autre côté, ça me permet de virer la commande tail -f.

Ce qu'il y a de rigolo dans tout ça, c'est qu'il est plus simple de gérer un centos 6 dans Docker qu'une version 7.

Problème systemd dans Docker (suite)

Vous allez me dire, dépose un fichier /etc/init.d lance le avec un shell et ça fonctionnera pareil. Et bien non puisque même si vous passez par le script directement, systemd l'intercepte et vous vous retrouvez avec le magnifique message d'erreur suivant :

Failed to get D-Bus connection: Operation not permitted

Le problème vient du fait que systemd a besoin d'accéder à tout un tas de chose pour gérer le contexte de lancement des process. La solution est donc de lancer le container en mode privileged avec les problèmes de sécurité que ça peut entraîner. Encore une fois, il ne s'agit pas d'un problème dans mon contexte mais je vous le déconseille vivement dans un autre contexte (en production par exemple).

Problème systemd dans Docker (suite)

Le lancement du premier container se passe très bien mais rapidement, je constate que certains process getty se lancent en se goinfrant 100% de CPU. En creusant un peu, je me rends compte que systemd (sous centos 7) lance automatiquement le service getty@tty1. Le premier lancement n'a pas de conséquence mais le problème apparaît au lancement des containers suivants. Ici, la solution est relativement simple : désactivation du service à la construction de l'image Docker. Ci-dessous l'instruction dans Dockerfile permettant de le réaliser :

RUN systemctl disable getty@tty1

Redémarrage des containers

Ayant fini par faire fonctionner mes playbooks, je me suis dit qu'il était temps de tester un arrêt/relance du container pour voir comment ça se comporte. Là encore, ça ne fonctionne pas vraiment bien. Ne comptez donc pas (pour l'instant) utiliser systemd pour gérer le démarrage des services de vos containers.

Connexion au container

J'en ai parlé il y a peu mais là, pas de problème : Ansible dispose d'un connecteur docker natif. Il faut donc se débrouiller pour affecter la valeur docker à la variable ansible_connection et le tour est joué.

Modélisation du réseau

Un autre truc qui a bien fonctionné est la capacité de Docker à manipuler des réseaux virtuels. Par le passé, sous Linux, cette partie pouvait être vraiment une gageure. J'avais écrit un article sur le sujet à l'époque. Il vous fallait gérer l'ajout de bridge, gérer la création des interfaces virtuelles de vos VM. Ici, rien de ce genre, vous créez votre réseau et Docker se charge du reste :

  • Création du bridge ;
  • Visibilité des containers dans le même réseau.

Ci-dessous un exemple de lancement de la création d'un réseau Docker :

docker network create demo

Tous les containers que nous rajouteront dans ce réseau par la suite auront automatiquement une résolution DNS disponible.

Pour conclure

Vous l'aurez compris, systemd et Docker ne sont pas les meilleurs amis du monde. Je trouve ça d'autant plus dommage que je pense qu'il y aurait clairement quelque chose d'intéressant à avoir si le support de ces deux produits avaient été meilleurs. Si comme moi vous avez utilisé des solutions à base de zone Solaris, vous pouvez passer votre chemin, il ne s'agit pas du même niveau de stabilité.

Une autre leçon à retenir de tout ceci est que Docker n'est clairement pas un remplacement d'une virtualisation classique. Ne partez surtout dans l'idée de réutiliser les mêmes outils sans passer du temps sur les adaptations préalables. Cette bascule doit absolument se faire dans le cadre d'une refonte de votre application et de sa gestion.

Pour le reste, l'outil Docker est très pratique à utiliser et vous permettra de prototyper très rapidement votre infra sans trop d'effort.

Un autre outil intéressant que je suis en train de creuser semble être Packer. En effet, cet outil permet de réutiliser vos playbooks pour la création de vos images Docker. Le gros avantage est que vous n'avez donc qu'un travail minimum pour créer indifféremment des machines classiques ou des containers et en vous garantissant d'avoir la même chose partout.

Attention toutefois, il s'agit pour moi d'une situation transitoire si vous avez décidé de partir sous Docker. Il reste très important de mon point de vue de simplifier vos installations et de passer par du Dockerfile. Idéalement, Ansible ne devrait vous servir que pour l'enchaînement de vos différentes tâches ou dans la gestion des machines qui ne sont pas dans le cloud (base de données par exemple) ou pour la gestion de l'infra Docker.

Concernant le code Ansible/Docker, vous pourrez retrouver des exemples dans le repository sur github du repository Meetup Ansible Paris #9. Bonne lecture !

dimanche 24 janvier 2016

Installation de docker et utilisation avec Ansible

Pour fêter la sortie d'Ansible v2, je vous propose un petit article sur l'utilisation de mes deux passe-temps du moment : Docker et Ansible

Le premier est très à la mode dans le monde de l'intégration continue. Sans rentrer dans les détails, j'y ai vu un moyen assez simple de faire des tests sur plusieurs types de distribution sans avoir à trop me casser la tête. D'autant que la dernière version d'Ansible embarque un connecteur natif docker.

Nous allons donc voir comment faire fonctionner tout ceci ensemble.

Installation de docker

De ce côté, rien de particulier à signaler. Sous Ubuntu, il vous faudra rajouter une source (en fonction de votre version d'Ubuntu) et sous CentOS, il vous faudra rajouter le repository extras dans vos dépendances.

Ci-dessous les instructions sous Ubuntu :

  • Suivre les instructions dans l'article d'installation de docker sous Ubuntu.
  • Installation de la librairie docker-py (nécessaire pour la communication d'Ansible avec docker) : pip install docker-py

Sous CentOS :

Voilà, passons à la suite.

Installation d'Ansible

Il vous faudra tout d'abord une version installée d'Ansible v2 (par la suite, j'utiliserai la 2.0.0.2). Pour vous en assurer, vous pouvez lancer la commande suivante : ansible --version

Pour installer une version, je vous laisse consulter l'article d'installation d'Ansible.

Création de container docker

Nous allons tout d'abord créer un playbook qui va nous permettre de créer nos images docker :

- name: "create docker container"
  hosts: docker-server
  gather_facts: no
  tasks:
    - docker: name={{item}} image=centos
              command="sleep 3600" state=started
      with_items: groups['docker-container']

NB : tous les exemples dans cet article sont récupérables à l'adresse suivante

L'astuce de ce playbook est de s'appuyer sur la variable groups['docker-container'] avec un with_items pour indiquer la liste des containers docker à créer.

Nous nous appuierons également sur le fichier d'inventaire suivant :

[docker-server]
localhost

[docker-container]
docker1
docker2

[docker-container:vars]
ansible_connection=docker
ansible_remote_user=root

Nous allons ensuite lancer la création de nos containers :

ansible-playbook -i hosts docker-example/create-docker.yml

Vous devriez obtenir, si tout se passe bien, le résultat suivant :

PLAY [create docker container] *************************

TASK [docker] ******************************************
changed: [localhost] => (item=docker1)
changed: [localhost] => (item=docker2)

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

Un rapide coup d'oeil avec docker ps nous indiquera ce qui vient d'être créé :

CONTAINER ID  IMAGE   COMMAND       CREATED         [...]   NAMES
d8bbda43b241  centos  "sleep 3600"  50 seconds ago  [...]   docker2
e34ed4b637b9  centos  "sleep 3600"  50 seconds ago  [...]   docker1

Utilisation des containers dans Ansible

Pour ainsi dire, le travail est terminé. Prenons tout de même un exemple de playbook :

- name: "create docker container"
  hosts: docker-container
  gather_facts: no
  tasks:
    - copy: dest=/tmp/{{item}} content=test
      with_items: [ test1, test2, test3 ]

Lançons ce playbook pour voir ce que nous allons obtenir :

PLAY [create docker container] *******************************

TASK [copy] **************************************************
changed: [docker2] => (item=test1)
changed: [docker2] => (item=test2)
changed: [docker2] => (item=test3)
changed: [docker1] => (item=test1)
changed: [docker1] => (item=test2)
changed: [docker1] => (item=test3)

PLAY RECAP **************************************************
docker1   : ok=1    changed=1    unreachable=0    failed=0   
docker2   : ok=1    changed=1    unreachable=0    failed=0   

Voilà, ça sera tout pour aujourd'hui !

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).

jeudi 9 octobre 2014

Comparaison de la gestion des FS avec Puppet et Ansible

Suite de mes aventures avec Ansible : aujourd'hui c'est du système Linux.

Les données du problème : je veux pouvoir gérer le LVM Linux et retailler mes FS à la demande.

La recette sous Puppet

Sous Puppet, en utilisant un fichier yaml/Hiera, l'écriture de tout ceci se ferait avec quelque chose de ce genre :

---
classes: ['lvm']

lvm::volume_groups:
  rootvg:
    physical_volumes:
      - /dev/sda2
    logical_volumes:
      tmp:
        size: 2G
        mountpath: /tmp
  datavg:
    physical_volumes:
      - /dev/sdb
    logical_volumes:
      test:
        size: 128
        mountpath: /test

Ne pas oublier non plus d'inclure le code suivant dans le fichier site.pp :

hiera_include('classes','')

Bien penser également à installer le module lvm, sinon ça ne marchera pas (puppetlabs-lvm).

Bon, sous Ansible, toutes ces belles choses n'existent pas (encore) en l'état mais sous forme de brique à assembler. On va voir comment recréer un mécanisme à peu près similaire.

La recette sous Ansible

Je me place dans le cadre d'un playbook Ansible avec un fichier system.yml se trouvant dans un répertoire de travail ~/ansible avec le contenu suivant :

---
# Playbook de gestion du système

- name: Propriétés communes système
  hosts: all
  remote_user: root

  roles:
    - system

Autre fichier présent : group_vars/all avec le contenu suivant :

---
# Variable applicable à tous

vgs:
  rootvg: "/dev/sda2"
  datavg: "/dev/sdb"

lvs:
  test:
    vg: rootvg
    mountpoint: /test
    size: 256

Ce fichier sera a spécialiser par groupe de machine mais est l'équivalent de notre fichier yaml/Hiera. Reste maintenant à écrire notre liste de tâche qui nous permettra de gérer ces FS (qu'on mettra dans le fichier roles/system/tasks/main.yml) :

---
- name: VG/PV Configuration
  lvg: vg={{ item.key }} pvs={{ item.value }} state=present
  with_dict: vgs

- name: LV creation
  lvol: vg={{ item.value.vg }} lv={{item.key}} size={{item.value.size}}
  register: task
  with_dict: lvs

- name: LV format
  filesystem: fstype=ext4 dev=/dev/mapper/{{item.value.vg}}-{{item.key}}
  with_dict: lvs

- name: Mount
  mount: name={{item.value.mountpoint}} src=/dev/mapper/{{item.value.vg}}-{{item.key}} dump=1 passno=2 fstype=ext4 state=mounted
  when: item.value.has_key('mountpoint')
  with_dict: lvs

- name: FS resize
  command: resize2fs /dev/mapper/{{item.item.value.vg}}-{{item.item.key}}
  with_items: task.results
  when: item.changed == True

NB : cette recette ne gère que des fs de type ext4. Si vous voulez gérer du xfs/btrfs/fegafatfs, il faudra changer la commande resize2fs par autre chose.

Le fonctionnement de la recette est assez simple : on enchaine les opérations (création du PV/VG, création du LV, formatage et montage) et on exécute la commande resize2fs qu'en cas de changement sur le LV (cf FS resize, ligne when: item.changed == True).

Reste maintenant à lancer notre playbook avec la commande ansible-playbook -i ./hosts ./system.yml. Vous devriez obtenir le résultat suivant :

PLAY [Propriétés communes système] ******************************

GATHERING FACTS *************************************************
ok: [machine1]

TASK: [common | VG/PV Configuration] ****************************
ok: [machine1] => (item={'key': 'datavg', 'value': '/dev/sdb'})
ok: [machine1] => (item={'key': 'rootvg', 'value': '/dev/sda2'})

TASK: [common | LV creation] ************************************
changed: [machine1] => (item={'key': 'test', 'value': ...})

TASK: [common | LV format] **************************************
changed: [machine1] => (item={'key': 'test', 'value': ...})

TASK: [common | Mount] ******************************************
changed: [machine1] => (item={'key': 'test', 'value': ...})

TASK: [common | FS resize] **************************************
changed: [machine1] => (item={u'msg': u'', 'item': ...})

PLAY RECAP ******************************************************
machine1        : ok=6    changed=4    unreachable=0    failed=0   

Changeons la taille de notre FS pour autre chose et relançons ansible pour vérifier la bonne prise en compte de tout ceci :

PLAY [Propriétés communes système] ******************************

GATHERING FACTS *************************************************
ok: [machine1]

[...]

TASK: [common | LV creation] ************************************
changed: [machine1] => (item={'key': 'test', 'value': ...})

[...]

TASK: [common | FS resize] **************************************
changed: [machine1] => (item={u'msg': u'', 'item': ...})

PLAY RECAP ******************************************************
machine1        : ok=6    changed=2    unreachable=0    failed=0   

Tout c'est bien passé. Il ne me reste plus qu'à vous souhaiter une bonne gestion de vos LV !