Ansible partie 2 : Créer un module simple pour installer des projets Go
La dernière fois, nous avions présenté le fonctionnement de base d’Ansible. Comment il était possible de définir une liste de tâches dans un playbook afin de les exécuter sur un serveur. Nous avions vu qu’il existait bon nombre de modules fournis de base permettant de réaliser des tâches très diverses. Mais que faire, si on veut faire quelque chose qui n’est pas prévu dans les modules prédéfinis ?
Deux solutions :
- A coup de tâches shell on exécute les commandes nécessaires, fastidieux, et peu maintenable.
- On réalise un programme séparé (sous la forme d’un script shell dans notre exemple) qui va exécuter la tâche désignée et va s’interfacer en tant que module avec Ansible.
Un des grands avantages d’Ansible est de permettre de créer des modules dans n’importe quel langage de programmation. Il suffit que votre code soit exécutable, l’interface avec Ansible se fait via des paramètres en ligne de commande et la sortie texte du programme. Si le shell est plus adapté, on peut écrire du shell. Si on aime le Python, c’est possible. Le Ruby, bien sûr. Perl, Lua ou même en C, pourquoi pas.
Interface avec les modules Ansible
Un module Ansible s’utilise comme suit :
- module_name: parameters
module_name
étant le nom du module, parameters
étant une chaîne de caractères libre, généralement sous la forme clef=valeur
, ceci n’étant pas obligatoire. Lorsqu’on utilise la forme clef=valeur
, il est possible d’utiliser les modules avec une autre syntaxe un peu plus détaillée :
- module_name:
key1: value1key2: value2
Ceci est strictement équivalent à écrire :
- module_name: key1=value1 key2=value2
Lorsque ce module sera appelé, un programme au doux nom de module_name
sera exécuté sur le serveur à déployer avec comme premier paramètre de ligne de commande un fichier contenant la lignekey1=value1 key2=value2
. Le programme n’a qu’à lire ce fichier pour comprendre ce qu’il doit faire, et pour les scripts shell, c’est simple, parce que c’est la syntaxe shell de définition de variables. Il suffit donc de sourcer ce fichier.
Pour indiquer le résultat, il doit écrire sur la sortie standard ou d’erreur indifféremment un fichier JSON contenant des variables résultat, par exemple :
{
"failed": false,
"changed": false
}
Ceci indique à Ansible qu’il n’y a pas eu d’erreur (variable failed
), que le système n’a pas été changé car il était déjà configuré (variable changed
). Si la sortie du programme n’est pas valide JSON, le module sera considéré comme ayant échoué.
Un module pour installer des projets go
Pour les besoins de cet article, nous allons faire un module qui installe un programme écrit dans le langage Go. Une fois qu’on a go
installé, il est très simple et rapide de compiler un programme Go. En effet, tout est généralement compilé statiquement sans dépendances externes, et l’outil go
permet de récupérer récursivement toutes les sources nécessaires à un programme. Par exemple, pour installer le projet GitHub go-ipfs, il suffit d’exécuter les commandes suivantes :
go get github.com/jbenet/go-ipfs/cmd/ipfs
go build github.com/jbenet/go-ipfs/cmd/ipfs
go install github.com/jbenet/go-ipfs/cmd/ipfs
Ceci va télécharger les sources dans $GOPATH/src
, compiler dans $GOPATH/pkg
et installer le programme ipfs dans $GOPATH/bin
.
Installation dans /usr/local avec stow
Une autre astuce réside dans l’installation dans /usr/local
en utilisant stow. C’est un programme qui permet de gérer le préfixe /usr/local
et avec des liens symboliques, permet de savoir quel fichier appartient à quelle installation. Le principe est simple, au lieu d’installer un programme dans /usr/local/{bin,lib,share,…}
directement, on l’installe dans /usr/local/stow/progname/{bin,lib,share,…}
. Ensuite on invoque stow et on lui demande de créer des liens symboliques dans /usr/local/{bin,lib,share,…}
pointant vers les fichiers équivalents dans /usr/local/stow/progname/{bin,lib,share,…}
.
Par exemple, si on considère le langage Lua installé, nous trouvons les liens symboliques suivants :
lrwxrwxrwx 1 root staff 21 août 6 14:17 /usr/local/bin/lua -> ../stow/lua52/bin/lua
lrwxrwxrwx 1 root staff 22 août 6 14:17 /usr/local/bin/luac -> ../stow/lua52/bin/luac
lrwxrwxrwx 1 root staff 31 mai 13 2014 /usr/local/include/lauxlib.h -> ../stow/lua52/include/lauxlib.h
lrwxrwxrwx 1 root staff 31 mai 13 2014 /usr/local/include/luaconf.h -> ../stow/lua52/include/luaconf.h
lrwxrwxrwx 1 root staff 27 mai 13 2014 /usr/local/include/lua.h -> ../stow/lua52/include/lua.h
lrwxrwxrwx 1 root staff 29 mai 13 2014 /usr/local/include/lua.hpp -> ../stow/lua52/include/lua.hpp
lrwxrwxrwx 1 root staff 30 mai 13 2014 /usr/local/include/lualib.h -> ../stow/lua52/include/lualib.h
lrwxrwxrwx 1 root staff 26 mai 13 2014 /usr/local/lib/liblua.a -> ../stow/lua52/lib/liblua.a
-rw-r--r-- 1 root staff 2204 nov. 16 2011 /usr/local/share/man/man1/lua.1
-rw-r--r-- 1 root staff 3071 nov. 16 2011 /usr/local/share/man/man1/luac.1
Cela nécessite d’installer les programmes avec un préfixe particulier. Généralement cela se fait avec ./configure –prefix=/usr/local/stow/progname
ou cmake -DCMAKE_INSTALL_PREFIX=/usr/local/stow/progname
.
Un module pour installer un projet go avec stow
Une première version du module pourrait être la suivante :
#!/bin/sh
exec 3>&1 >/dev/null 2>&1
. "$1"
export GOPATH="/usr/local/src/$name"
STOWDIR="/usr/local/stow/$name"
rm -rf "$GOPATH/bin" "$STOWDIR/bin"
mkdir -p "$STOWDIR/bin" "$GOPATH"
ln -s "$STOWDIR/bin" "$GOPATH/bin"
go get -u "$package"
go build "$package"
go install "$package"
cd /usr/local/stow/
stow -R "$name"
cat <<EOF >&3
{
"failed": false,
"changed": true
}
EOF
exit 0
Ce module s’utilise ainsi :
- go-install: name=go-ipfs package=github.com/jbenet/go-ipfs/cmd/ipfs
Ce script va d’abord supprimer le dossier bin
, créer un dossier /usr/local/stow/$name/bin
et faire pointer /usr/local/src/$name/bin
vers ce dernier dossier. Ainsi go install
placera directement les fichiers dans /usr/local/stow/$name/bin
.
La commande pour installer un paquet stow doit être exécutée dans /usr/local/stow
. C’est stow -R $name
(-R
comme Restow).
Ne pas oublier la commande exec 3>&1 >/dev/null 2>&1
qui ferme la sortie standard et d’erreur afin d’éviter que les programmes ne polluent le résultat JSON, et 3>&1
(avant les autres) qui permet d’ouvrir le descripteur de fichier numéro 3 comme une nouvelle sortie standard. Le résultat JSON sera copié sur ce descripteur de fichier.
Un peu de robustesse
Afin de gérer les erreurs, le cas où les commandes exécutées retournent un code d’erreur différent de zéro, nous allons utiliser bash au lieu du vénérable shell POSIX sh et utiliser la commande trap. Nous allons aussi initialiser les variables dans le cas où notre environnement n’est pas propre, et donner une valeur par défaut à la variable $name
:
#!/bin/bash
failed=false
res_code=0
msg=Success
trap 'failed=true res_code=1 msg="Failed at line $LINENO"' ERR
exec 3>&1 >/dev/null 2>&1
name=
package=
. "$1"
: ${name:="$(basename $package)"}
export GOPATH="/usr/local/src/$name"
STOWDIR="/usr/local/stow/$name"
rm -rf "$GOPATH/bin" "$STOWDIR/bin"
mkdir -p "$STOWDIR/bin" "$GOPATH"
ln -s "$STOWDIR/bin" "$GOPATH/bin"
go get -u "$package"
go build "$package"
go install "$package"
cd /usr/local/stow/
stow -R "$name"
cat <<EOF >&3
{
"failed": $failed,
"changed": true,
"msg": "$msg"
}
EOF
exit $res_code
Utilisation du module
Pour utiliser le module, il doit être placé dans un dossier library/
au coté du playbook. Nous avons donc les fichiers suivants :
ipfs.yml
hosts
library/go-install
Notre playbook ipfs.yml
pourrait être ainsi :
---
- hosts: perrin
sudo: yes
tasks:
- go-install: name=go-ipfs package=github.com/jbenet/go-ipfs/cmd/ipfs
L’exécution donne ceci :
PLAY [perrin] *****************************************************************
GATHERING FACTS ***************************************************************
ok: [perrin.mildred.fr]
TASK: [go-install name=go-ipfs package=github.com/jbenet/go-ipfs/cmd/ipfs] ****
changed: [perrin.mildred.fr]
PLAY RECAP ********************************************************************
perrin.mildred.fr : ok=2 changed=1 unreachable=0 failed=0
Vous avez maintenant pu voir le fonctionnement des modules Ansible, je vous encourage à jouer avec. Si vous étiez à l’aise avec le shell, et redoutiez les outils de déploiement automatique — parce-qu’il faut l’avouer, le langage est parfois bien plus compliqué — vous êtes maintenant armé pour utiliser Ansible en toute simplicité. Dans un prochain article, nous verrons comment structurer des playbooks Ansible lorsque vous souhaitez mixer différentes tâches.