Par défaut, la configuration des scénarios à jouer sur les serveurs clients de Puppet se fait au niveau du fichier “nodes.pp”. Cette solution atteint vite ses limites dès lors que vous avez de nombreux clients, paramètres, classes.

En effet, ce fichier devient vite assez dense rendant sa lecture difficile et la porte ouverte aux erreurs qui peuvent rapidement se transformer en actions non désirées sur vos clients ! De même, connaître d’un coup d’oeil l’ensemble des scénarios joués ainsi que leurs paramètres devient compliqué. Puppet propose d’autres solutions comme par exemple utiliser un LDAP ou un External Node Classifier (ENC), ce que nous allons voir dans cet article.

Je vais reprendre ici la définition d’un ENC que vous trouverez sur la documentation de Puppet tout en rajoutant des exemples précis sur l’utilisation de ressources de type “define” avec l’utilisation de tables de hachage en paramètre.

ENC ? Ce que vous devez savoir

Un ENC n’est rien d’autre qu’un programme appelé par le puppetmasterd au moment où un client se présente pour obtenir son catalogue. Puppet va alors appeler le programme défini avec pour paramètre le hostname (en fqdn). Ce programme peut être écrit dans le langage de votre choix. Dans votre ENC, vous pouvez utiliser le backend de votre choix comme source de donnée. La seule obligation est de fournir les informations en YAML. Il est également possible d’utiliser Puppet Dashboard ou The Foreman comme ENC.

Il y a cependant quelques limites importantes quand on utilise un ENC :
- Avec un ENC, vous ne pouvez que déclarer des classes (classiques et avec paramètre), définir des variables globales et définir un environnement (bien que visiblement toujours mal supporté à ce jour).
- Vous ne pouvez donc pas utiliser, en l’état, vos ressources de type “define” !
- Vous ne pouvez pas déclarer de relations entre vos différentes classes.

Le YAML produit par l’ENC

Je vais reprendre les exemples de Puppet en détaillant un peu. Imaginons 4 classes : common, puppet, dns, ntp.

Dans le “nodes.pp”, cela ressemblerait donc à :

node default {
include common
include puppet
include dns
include ntp
}

Votre ENC produira lui le résultat suivant :

classes:
common:
puppet:
dns:
ntp:

Avec une classe paramétrée

Si vous utilisez une classe paramétrée (Parameterized Classes), on passe de :

class { 'ntp':
ntpserver => ['0.pool.ntp.org','1.pool.ntp.org']
}

à :

classes:
common:
puppet:
ntp:
ntpserver:
- 0.pool.ntp.org
- 1.pool.ntp.org

Déclarer des variables globales

Il est possible de déclarer uniquement des variables globales (top scope variables). Dans votre “nodes.pp” ou “site.pp” :

$var1 = 1
$var2 = "test"
$var3 = ["a", 1]
$var4 = { 'ma_ref1' => { 'cle1' => 'valeur1', 'cle2' => 'valeur2' },\
'ma_ref2' => { 'cle1' => 'valeur1', 'cle2' => 'valeur2' } }

Votre ENC affichera le YAML suivant :

parameters:
var1: 1
var2: test
var3:
- a
- 1
var4:
ma_ref1:
cle1: valeur1
cle2: valeur2
ma_ref2:
cle1: valeur1
cle2: valeur2

Comment faire pour les ressources “define”

Par défaut, vous indiquez uniquement les classes associées à vos clients. Si vous avez dans votre “nodes.pp” une référence à une ressource de type “define”, vous ne pouvez plus l’utiliser directement avec votre ENC. Pour être clair, imaginons un module “users” et son manifest “create” :

# cat /etc/puppet/modules/users/manifests/create.pp
define users::create($user, $path) {
[...]
}

L’utilisation se fait de cette façon dans le “nodes.pp” :

user::create { 'user1':
user => 'user1',
path => '/home/user1'
}
user::create { 'user2':
user => 'user2',
path => '/home/user2'
}

Il n’y a pas de solution directe (à ce jour) pour reproduire cette fonctionnalité dans l’ENC. Vous ne pouvez pas définir les variables et appeler user::create comme on appelle une classe. Il faudra utiliser la fonction _createresources. Cette fonction est disponible nativement depuis la version 2.7 de Puppet. Pour les versions précédentes, il faudra passer par un module.

Grace à cette fonction, on va finalement utiliser une classe qui aura pour mission de construire dynamiquement les différents appels à user:: via create_resources.

Exemple :

Mon manifest create.pp reste identique. Il n’y a pas de modification à lui apporter. Il a toujours besoin de la variable $user et $path. On va créer une nouvelle classe manage :

# cat /etc/puppet/modules/user/manifests/manage.pp
class user::manage ($user_list) inherits user {
create_resources('user::create',$user_list)
}

On peut maintenant appeler la classe user::manage dans l’ENC. Il faut déclarer la variable user_list nécessaire à la classe user::manage pour finalement fournir à user::create les variables user et path. Le YAML produit ressemblera à :

classes:
user::manage:
user_list:
user1:
user: user1
path: /home/user1
user2:
user: user2
path: /home/user2

Un exemple en python

Un petit exemple d’un ENC basique en Python.

#!/usr/bin/env python
import sys
import re
from yaml import dump
host = sys.argv[1]
# Les classes par defaut
classes = [
'common',
'puppet',
'dns',
'ntp'
]
# Si le client Puppet est mon_host
# on rajoute la classe user::create
if host == "mon_host":
classes.append('user::manage')
# On transforme la liste des classes en dictionnaire
param_classes = {}
for cl in classes:
param_classes[cl] = {}
# Les classes avec parametres
param_classes['ntp'] = { 'ntpserver': [
'0.pool.ntp.org',
'1.pool.ntp.org'
] }
# Dictionnaire des parametres
parameters = {}
# Des parametres precis pour mon_host et la classe user::manage
if host == "mon_host":
param_classes['user::manage']['user_list'] = {
'user1': { 'user':'user1','path':'/home/user1'},
'user2': { 'user':'user2','path':'/home/user2'},
}
output = {
'classes' : param_classes,
'parameters': parameters
}
print dump(output,default_flow_style=False);

On obtient finalement la sortie suivante :

# /usr/local/bin/test_enc.py mon_host
classes:
common: {}
dns: {}
ntp:
ntpserver:
- 0.pool.ntp.org
- 1.pool.ntp.org
puppet: {}
user::manage:
user_list:
user1:
path: /home/user1
user: user1
user2:
path: /home/user2
user: user2
parameters: {}

Et pour un serveur différent de mon_host :

# /usr/local/bin/test_enc.py serveur1
classes:
common: {}
dns: {}
ntp:
ntpserver:
- 0.pool.ntp.org
- 1.pool.ntp.org
puppet: {}
parameters: {}

Vous pouvez également utiliser des classes classiques et des variables globales visibles, donc, au niveau de “parameters” dans votre ENC. Cela fonctionnera de la même façon mais l’inconvénient majeur de cette solution est que votre variable est exposée à toutes les classes. Tant que vous maîtrisez l’ensemble de vos modules et de vos classes, vous pouvez vous assurer de ne pas écraser cette variable. Dès lors que vous utilisez des modules/classes écrites par d’autres personnes, le risque devient beaucoup plus important. C’est une des raisons de l’apparition des Parameterized Classes que je vous encourage, ici, à utiliser.

Activer l’ENC

Il ne vous reste plus qu’à indiquer à Puppet d’utiliser votre ENC. Dans votre fichier de configuration /etc/puppet/puppet.conf, rajoutez :

[master]
node_terminus = exec
external_nodes = /usr/local/bin/test_enc.py

Relancez ensuite votre Puppet master.

C’est terminé ! Pour aller plus loin, vous pouvez par exemple fournir la liste des classes, des paramètres et les groupes de serveurs via une base de données alimentée par une IHM. L’extraction sera réalisée toujours par votre ENC du langage de votre choix et qui produira le YAML pour Puppet. La gestion de la configuration du serveur client deviendra aussi simple que de choisir au niveau de l’IHM une liste de classes et de paramètres.