Article publié dans Linux Magazine 95, juin 2007.
Copyright © 2007 - Nicolas Chuche
Administrer un seul serveur est une tâche assez simple. Administrer un parc de serveurs est déjà beaucoup plus difficile et la tâche devient carrément complexe quand les systèmes sont hétérogènes (ce qui sera toujours le cas au bout de quelques années au moins). Imaginez que vous vouliez changer l'IP de vos serveurs DNS, il faudra vous connecter sur chaque serveur pour modifier le fichier /etc/resolv.conf. Il en est de même pour vérifier les droits de certains fichiers critiques, pour s'assurer qu'un programme est lancé sur tels serveurs voire pour diffuser un patch sur tous vos serveurs AIX 5.1.
Pour répondre à ces problèmes qui ont occupé et occupent toujours des
bataillons d'administrateurs système[1], il existe plusieurs pistes
allant de l'utilisation d'outils de provisioning du commerce (chers et
pas toujours très efficaces) au développement d'outils ad-hoc ciselés à la main par
l'administrateur local aux solutions plus ou moins complètes (PIKT,
cfengine, LCFG, pica, etc.), en passant par l'utilisation de briques
communes agencées ensemble par des scripts (cvs
, rsync
, ssh
et
consorts). Chaque administrateur et chaque centre informatique s'est fait
sa philosophie et a implanté sa solution.
Dans cet article, nous allons découvrir ensemble un des outils les plus connus du domaine, à savoir cfengine. Nous allons d'abord parcourir ensemble le langage et la grammaire de cfengine pour ensuite voir quelques exemples réels de scripts cfengine et enfin aborder une partie méthodologie.
cfengine peut être vu comme langage de très haut niveau conçu pour administrer et configurer un ensemble d'ordinateurs. A ce titre il réalise une abstraction d'opération de base comme la manipulation des IPs, de la configuration DNS ou NTP, etc. Ceci est un de ses points forts, puisque la même fonction cfengine permet de manipuler des choses décrites différemment selon le système : une IP ne s'affecte pas de la même façon sous Linux que Solaris ou Windows...
C'est l'œuvre d'un universitaire norvégien, Mark Burgess, enseignant l'administration réseau et système à l'université d'Oslo. Il intervient régulièrement sur ces sujets dans les conférences internationales telles que LISA, Usenix et OSCON. Son « dada » est de parler du « système immunitaire » des ordinateurs. L'idée est de considérer qu'un ordinateur est une forme d'organisme vivant qui doit lutter contre des « agressions » et s'adapter pour revenir dans état sain : vider les filesystems qui se remplissent, accepter la déclaration de nouveaux serveurs DNS, installer de nouveaux packages, s'assurer que des processus s'exécutent, etc. Le système doit devenir autonome, et réagir seul pour rester « en bonne santé ». De notre point de vue d'administrateur système, nous sommes alors libérés pour nous consacrer à des tâches « plus nobles », dites à valeur ajoutées, telles que la définition de l'état correct du système ou l'analyse et la correction des problèmes nouveaux, qui devront ensuite être décrits en cfengine pour se corriger automatiquement. On entre alors dans un processus incrémental d'amélioration de la qualité, en capitalisant sur les échecs, les erreurs et les problèmes rencontrés pour éviter qu'ils ne se reproduisent.
Si cfengine n'est pas idéal pour administrer les systèmes Microsoft (même s'il a été porté sous différentes versions de Windows), il tourne sur la majorité des UNIX standards et moins standards (Linux bien sûr, AIX, SCO, MacOS X, etc.). Le seul bémol à ce sujet concerne les UNIX anciens sur lesquels il risque d'être parfois difficile de compiler les pré-requis.
cfengine bénéficie d'une bonne communauté d'utilisateurs présente sur
le web (cfwiki[2]) et à travers des listes de discussion
(help-cfengine@gnu.org
).
La version source de Cfengine est diffusé sur le site de Cfengine[3] mais il y a des chances que votre distribution propose une version packagées.
Si vous êtes sous Debian Sarge vous pouvez vous contenter de faire un
apt-get
:
# apt-get install cfengine2 cfengine2-doc
Mandriva dispose également d'une version récente installable en faisant tout simplement :
# urpmi cfengine
Pour RHEL (Red Hat Enterprise Linux), versions 4 ou 5, qui ne livrent pas cfengine au contraire de Fedora, vous pouvez utiliser les versions récentes publiées au sein du projet EPEL[4], en ajoutant la source yum soit à up2date (RHEL4), soit à yum (RHEL5).
Si votre distribution ne fournit pas de package il vous faudra le compiler.
Les deux pré-requis indispensables sont BerkeleyDB 3.2 minimum et OpenSSL 0.9.7 minimum. S'ils ne sont pas présents sur votre système ni sous forme de paquetage, vous pouvez les télécharger respectivement sur http://www.sleepycat.com/ et http://www.openssl.org/. Ensuite l'installation par défaut fonctionne parfaitement :
# ./configure # make # make install
Suite à l'installation, il vous faudra générer un bi-clé (couple de clés publique/privée) pour faire communiquer les processus entre eux :
# cfkey
C'est installé. Ou du moins installé pour une utilisation locale qui nous suffira pour l'instant pour découvrir cfengine. Nous verrons plus loin comment installer un parc de serveurs en réseau.
cfengine s'appuie essentiellement sur 4 programmes : cfkey
, cfagent
,
cfservd
et cfrun
. D'autres existent (cfgetenv
, ...), mais qui n'ont
d'intérêt que dans une utilisation avancée et sortent du
cadre de cet article.
cfagent
est le programme qui va analyser et exécuter les
fichiers de configuration. Il se configure avec les fichiers
update.conf et cfagent.conf qu'il va chercher, par défaut,
dans le répertoire de configuration défini plus bas. On peut lui
en spécifier un autre fichier avec l'option -f
.
cfagent
peut être vu comme un interpréteur au même titre que
Perl ;
cfrun
est le programme qui va permettre de lancer les cfagent
à
distance ;
cfservd
est un « daemon ». Il fait office de serveur de
fichiers intégré à cfengine. Il permet également de lancer cfagent
sur une sollicitation réseau ;
cfkey
génère un bi-clé utilisé pour sécuriser la connexion entre
cfagent
/cfrun
et cfservd
;
Depuis la version 2 de cfengine sortie autour de 2002, tous les fichiers importants sont réunis dans /var/cfengine. Si votre distribution les met ailleurs (/var/lib/cfengine2 pour debian par exemple), je ne saurais trop vous conseiller de faire un lien vers ce répertoire afin d'harmoniser toutes vos configurations. Nous verrons plus loin dans cet article un script pour le faire sur les clients mais pour le serveur faites-le dès maintenant.
Dans le cadre de cet article nous ne verrons que trois répertoires :
/var/cfengine/bin contient les fichiers exécutables
/var/cfengine/inputs contient les fichiers de configuration
/var/cfengine/ppkeys contient les bi-clés utilisés pour sécuriser les communications client/serveur
Si vous utilisez cfengine sous un autre compte que root, le répertoire de base sera ~/.cfagent dans lequel vous retrouverez les répertoires ci-dessus.
Les fichiers de configuration se trouvent donc normalement dans /var/cfengine/inputs. Voici les principaux :
cfagent.conf est le fichier de configuration de... cfagent
;
update.conf est également utilisé par cfagent
. C'est un
fichier minimum permettant d'amorcer le démarrage de cfagent
(récupération d'une configuration récente et exempte de bug
par exemple). Il est lancé avant cfagent.conf ;
cfservd.conf est le fichier de configuration de cfserv
. On va y
indiquer par exemple quels sont les serveurs ayant le droit de se connecter
et quels répertoires ils pourront récupérer ;
cfrun.conf permet de définir la liste des serveurs à contacter
quand on utilise cfrun
.
Dans la suite de cet article vous essayerez de nombreux fichiers de
configuration, cfengine est conçu de façon à ne pas lancer en
boucle les commandes afin de ne pas saturer le système. En phase de
test, vous pouvez utilisez l'option -K
de cfagent
qui
désactive cette sécurité. Toujours pour éviter de
surcharger le système, cfengine permet d'introduire un délai
aléatoire avant le lancement réel, de cette façon, même si
tous vos serveurs lancent cfengine à heure fixe toutes les heures,
ils ne se connecteront pas tous au serveur centrale au même
instant. Pour désactiver ce délai vous devrez utiliser utiliser
l'option -q
.
Attention à bien respecter les espacements (présents ou absents) autour des caractères ':()' ;-)
# cat /var/cfengine/inputs/cfagent.conf control: actionsequence = ( shellcommands tidy ) shellcommands: "/usr/bin/id" tidy: /tmp pattern=*~ recurse=inf age=2
Ce fichier de configuration est composé de trois sections :
control
, shellcommands
et tidy
. La première permet de
configurer le comportement de cfagent
et en l'occurrence de fixer
l'ordre d'exécution des actions : shellcommands
puis
tidy
. La deuxième section indique qu'il faut lancer la
shellcommands
(commande externe à cfengine, à la manière
de "system" en Perl) /usr/bin/id. La troisième section, qu'il
faut lancer l'action tidy
(suppression) sur le répertoire
/tmp sur les fichiers dont le nom se termine par '~' (pattern=*~),
vieux de plus de deux jours (age=2), et en analysant également les
sous-répertoires (recurse=inf) Pour exécuter ce fichier, il
suffit de faire :
# cfagent -q -K cfengine::usr/bin/id: uid=0(root) gid=0(root) groups=0(root)
Comme on le voit, cfagent
exécute la commande id
. Si d'aventure
vous aviez des fichiers correspondant au motif recherché dans le
répertoire /tmp, ils auront été supprimés.
Ce simple fichier montre déjà une partie de la puissance de cfengine : en très peu de lignes, nous avons pu exprimer des tâches de très haut niveau.
cfengine est utilisé pour gérer des parcs informatiques de plusieurs dizaines à plusieurs centaines de serveurs hétérogènes. Si chaque serveur devait avoir son fichier de configuration spécifique, le système deviendrait vite inmaintenable.
Pour résoudre ce problème et permettre l'utilisation d'un fichier de configuration pour plusieurs systèmes, cfengine implante la notion de classes. Celles-ci permettent d'exécuter certaines parties des scripts sous certaines conditions définies par l'environnement ou par l'utilisateur, à la manière du 'if' des langages de programmation :
si (la classe CCCC est définie) alors ... si (la classe DDD est définie et la classe EEE ne l'est pas) alors ...
Quelques classes dites « dures » (car dépendantes du système) sont le
type de système (AIX, Debian, Red Hat, etc.), l'architecture (i386,
i686, ppc, etc.) tandis que d'autres le sont moins comme par exemple la
classe horaire (Hr13
pour indiquer que nous sommes dans la 13ème
heure), l'année (Yr2007
), l'adresse IP d'une interface, etc.
Il existe enfin une classe générique any
qui, comme son nom
l'indique, est tout le temps vraie. Elle est utilisée implicitement
si vous ne spécifiez pas de classe et vous pouvez bien sur
l'utiliser explicitement.
On peut voir toutes les classes prédefinies en lançant la commande :
# cfagent -v -p
Sur mon serveur cela donne :
Defined Classes = ( 10_74_0 10_74_0_1 172_16_235 172_16_235_1 192_168_0 192_168_0_2 192_168_111 192_168_111_1 32_bit Day1 Hr15 Hr15_Q4 May Min55_00 Min58 Q4 Tuesday Ubuntu_7_04__n__l_ VMware Yr2007 addr_ any cfengine_2 cfengine_2_1 cfengine_2_1_20 compiled_on_linux_gnu debian debian_4 debian_4_0 fe80__202_e3ff_fe04_44f8 fe80__211_2fff_fe0d_b6f9 fe80__250_56ff_fec0_1 fe80__250_56ff_fec0_8 gaspard gaspard_local [...] net_iface_vmnet8 undefined_domain )
Ces classes s'utilisent en ajoutant le nom de la classe désirée, immédiatement suivi de :: (attention à ne pas mettre d'espaces) juste avant l'action à réaliser ou à ne pas réaliser :
control: actionsequence = ( shellcommands ) shellcommands: linux:: "/usr/local/bin/backup"
Cette séquence permet de lancer backup
uniquement sur les
serveurs où la classe linux
est définie. Si, comme cela
devrait être le cas, cfagent
est lancé régulièrement
(par exemple toutes les heures), backup
sera lancé à chaque
fois, ce qui n'est pas forcément l'effet
désiré. Heureusement, il est possible de combiner les classes
grâce aux opérateurs « et » (.
) et « ou »
(|
ou ||
pour ceux qui sont habitués à cette syntaxe dans
leur langage de programmation). Le « et » est prioritaire sur
le « ou » mais les parenthèses ()
peuvent être
utilisées pour changer les priorités. La négation d'une
classe est faite avec « ! » qui a la plus forte
précédence. Enfin, une portion "conditionnelle" se termine avec
la prochaine condition, ou la fin de la section cfengine dans laquelle
elle apparaît. En voici une illustration :
control: actionsequence = ( shellcommands ) shellcommands: redhat.Hr01:: "/usr/local/bin/backup" linux.!redhat.Hr23:: "/usr/local/bin/backup" (aix|sun).Hr02:: "/usr/local/bin/backup"
lancera :
/usr/local/bin/backup
sur toutes les Red Hat (classe redhat
) pendant
la première heure de la journée (donc entre minuit et 0h59).
/usr/local/bin/backup
sur tous les Linux non Red Hat entre 23h et
23h59.
/usr/local/bin/backup
sur les AIX et les Sun entre 1h et 1h59.
Notez bien que cfagent réalisera l'action à chaque fois qu'il sera lancé dans le bon créneau horaire...
On peut également ajouter des classes utilisateurs :
control: actionsequence = ( files ) classes: oracle = ( FileExists(/etc/oratab) ) alerts: oracle:: "oracle est defini" # cfagent cfengine:: oracle est defini
Comme vous l'avez sans doute déjà compris, la classe oracle
sera
définie si et seulement si le fichier /etc/oratab existe.
Un certain nombre de commandes du type IsDir
, IsLink
, IsPlain
et d'autres étendent ces possibilités. Vous pouvez les trouver dans la
documentation de cfengine[5]
Comme vu précédemment, la syntaxe générale des fichiers de configuration est donc :
section: classe:: directive
Les sections peuvent être découpées en petit bout et s'entrelacer entre elles :
section1: classe1:: directive section2: classe2:: directive section1: classe2:: directive
Néanmoins, même si cela peut s'avérer plus lisible,
l'intégralité de chaque section sera exécutée en une seul
fois au moment défini dans actionsequence
et non en respectant
l'entrelacement.
Les sections sont tout ce que cfengine sait faire. Elles se regroupent
en deux sortes, les sections implicites qui permettent de contrôler et
modifier le fonctionnement de cfengine (en créant des classes par
exemple) et les sections explicites qui exécutent effectivement
quelque chose et modifient l'état du système. Les sections explicites
doivent être déclarées dans une directive actionsequence
pour être
exécutées. L'ordre d'exécution sera celui de cette même directive.
alerts
permet de remonter des messages quand une classe est active.
classes
permet de définir des classes de serveurs (groups
est un
synonyme mais n'utilisez qu'un seul des deux).
control
, comme son nom l'indique, contrôle le comportement de
cfengine. Cette action sert à fixer les valeurs par défaut ainsi que
des variables.
filters
permet de définir des filtres utilisables dans la copie,
l'édition de fichiers, la recherche de processus...
ignore
permet d'ignorer des répertoires dans les commandes qui
normalement les traverseraient (par exemple tidy
, file
et
copy
que nous verrons ci-dessous).
import
importe le contenu des fichiers de configuration dans le
fichier courant. Attention : ce n'est pas « include » comme
dans les langages dont vous avez l'habitude, ou alors voyez-le comme
un include réalisé après la dernière ligne du fichier
où vous l'avez programmé. Ces imports sont réalisés dans
l'ordre.
Un conséquence direct de ce que nous venons de dire est que si quelque chose est défini dans un des fichiers importés, elle n'est pas disponible dans le fichier qui importe (c'est vrai entre autre pour les classes que nous avons vu précédemment) mais sera disponible dans les fichiers importés après.
Comme déjà évoqué, ce sont ces actions qui agissent
effectivement sur le système. L'ordre d'exécution est défini
par la directive actionsequence
.
copy
copie des fichiers entre systèmes de fichiers locaux ou à
travers le réseau en utilisant cfservd
.
disable
désactive des fichiers en les déplaçant (renommage).
disks
vérifie qu'un système de fichiers existe et contient assez d'espace
disponible.
directories
crée des répertoires.
editfiles
modifie des fichiers.
files
crée, vérifie l'existence, change les droits de fichiers.
links
crée des liens.
packages
vérifie qu'un paquetage est installé.
processes
gère les processus.
shellcommands
exécute des commandes externes, scripts, etc.
tidy
supprime des fichiers respectant certaines conditions (motifs
dans le nom du fichier, âge, etc.)
Cette liste n'est pas exhaustive. Vous trouverez également dans la
documentation[4] des commandes comme homeservers
, binservers
,
miscmounts
, resolve
et quelques autres. Je vous déconseille de
les utiliser, au moins dans un premier temps. Tout peut être fait sans
ces commandes.
Dans cette partie, il nous faudra au moins deux machines, l'une faisant
office de serveur et la seconde de client. Nous avons également besoin
d'un nom de domaine, même fictif (mon.tld
fera l'affaire), et d'une
plage d'adresses IP privées (172.16.235.0/24 dans le cas présent). Le
serveur s'appelle maitre.mon.tld
et a pour adresse 172.16.235.1 et le
client 172.16.235.128 (les hasards du DHCP).
Comme nous l'avons vu précédemment, cfservd
est la partie serveur de
cfengine. Deux programmes peuvent communiquer avec ce dernier :
cfagent
pour échanger des fichiers et cfrun
pour lancer un
cfagent
sur un ou des serveurs distants.
Afin de sécuriser les échanges, cfengine v2 utilise un bi-clé. En
fait, comme pour SSH, ce bi-clé ne sert qu'à vérifier l'identité du
correspondant et à échanger une clé de session, le reste de la
communication étant chiffrée avec cette clé de session par l'algorithme
de chiffrement symétrique Blowfish. Le bi-clé est généré par cfkey
et
stocké par défaut dans le répertoire /var/cfengine/ppkeys, il est
composé de deux fichiers localhost.priv et localhost.pub. Toute
la difficulté réside dans sa diffusion sur l'ensemble des serveurs
concernés qui peut être manuelle ou automatisée par cfengine.
Pour la diffusion manuelle, il suffit de copier le fichier localhost.pub du serveur maître dans le répertoire /var/cfengine/ppkeys de chaque serveur client sous le nom root-IP.pub, soit dans notre cas root-172.16.235.1.pub et de copier les fichiers localhost.pub de chaque serveur client dans le répertoire /var/cfengine/ppkeys sous le nom root-IP.pub soit dans notre cas root-172.16.235.128.pub.
Cette première méthode est simple à mettre en œuvre mais pose certains problèmes quand il s'agit de répliquer les clés sur un grand nombre de serveurs. cfengine permet de désactiver le mécanisme de vérification du bi-clé, soit un bref moment, le temps d'échanger ce fameux bi-clé, soit définitivement, ce qui n'est pas recommandé sauf si vous êtes dans un environnement sûr.
cfservd
La syntaxe de cfservd.conf
se rapproche de celle de cfagent.conf
sans toutefois être exactement la même :
# cat /var/cfengine/inputs/cfservd.conf control: # le domaine utilisé domain = ( mon.tld ) # les clés des agents de ce domaine sont acceptées # cette ligne permet d'autoriser la connexion de nouveaux agents TrustKeysFrom = ( 172.16.235.0/24 ) # seul root est autorisé à se connecter AllowUsers = ( root ) # définit le temps en minutes avant qu'une action puisse à nouveau # avoir lieu IfElapsed = ( 5 ) # le temps en minutes alloué à une action avant qu'elle ne soit considérée # comme bloquée par cfengine. Au-delà de ce temps cfengine est tué ExpireAfter = ( 15 ) # le nombre maximum de connexions autorisées sur le serveur MaxConnections = ( 10 ) # un client a-t-il le droit de se connecter plusieurs fois simultanément MultipleConnections = ( true ) grant: /var/cfengine/inputs/ 172.16.235.0/24 # attention, si /var/cfengine/inputs est un lien, il faut # autoriser le répertoire sur lequel il pointe. Sur # debian/ubuntu, il faudra donc quelque chose de la forme : /etc/cfengine/ 172.16.235.0/24 # et sur mandriva quelque chose comme ça : /var/lib/cfengine/inputs 172.16.235.0/24
La commande TrustKeysFrom
permet à l'agent encore inconnu du
serveur de s'authentifier. Le serveur récupère la clé publique de cet
agent et la stocke dans le répertoire défini ci-dessus. Attention, si
l'agent a changé de clé, le serveur ne l'acceptera pas, il faudra
alors manuellement supprimer l'ancienne clé de cet agent sur le
serveur.
L'action grant
liste les répertoires auxquels les serveurs ont
le droit d'accéder. Dans le cas présent, tous les serveurs du
sous-réseau 172.16.235.0/24
ont le droit d'accéder à
/var/cfengine/inputs pour récupérer leur configuration. Il
est normalement préférable d'utiliser un nom de domaine dans la
section grant
:
grant: /var/cfengine/inputs/ *.mon.tld
Mais pour simplifier un peu nous allons utiliser les sous-réseau.
Ca y est, vous pouvez lancer le daemon cfservd en lançant tout
simplement cfservd
sous root.
Nous allons bien sûr nous servir de cfengine pour diffuser ses
propres fichiers de configuration. Comme nous l'avons vu au début
de cet article, cfagent
va lire dans l'ordre les fichiers
update.conf puis cfagent.conf. Le premier fichier sert à
initialiser cfengine (récupération des fichiers de configuration
récents) et doit rester le plus simple et robuste possible. En
voici un qui devrait convenir pour commencer. Placez-le sur le serveur
toujours au même endroit :
# cat /var/cfengine/inputs/update.conf control: actionsequence = ( copy ) domain = ( mon.tld ) policyhost = ( 172.16.235.1 ) copy: # attention, les "/" finaux sont importants si vous avez des liens /var/cfengine/inputs/ dest=/var/cfengine/inputs/ r=inf exclude=*~ server=$(policyhost) trustkey=true
Le paramètre trustkey
autorise l'agent à accepter la clé d'un
serveur s'il ne le connaît pas encore. Comme pour la commande
TrustKeyFrom
, elle ne permet pas d'écraser une clé déjà existante :
il faut d'abord la supprimer à la main
L'action copy
décrit que le client doit copier récursivement le
répertoire /etc/cfengine sur le serveur déclaré dans la variable
policyhost
en excluant les fichiers respectant le motif *~
(qui sont
des fichiers utilisés par un certain nombre d'éditeurs pour conserver
les dernières modifications).
Pourquoi passer par une variable policyhost
et ne pas mettre
directement :
server=maitre.mon.tld
Parce que cacher au milieu d'un programme une donnée aussi importante est rarement une bonne idée que cela soit dans des scripts cfengine ou dans tout autre langage de programmation. Pensez aux personnes qui devront assurer la maintenance de votre cfengine.
Une fois ce fichier mis en place, modifiez-le le moins possible et vérifiez scrupuleusement ces modifications sous peine de devoir « pousser » à la main une configuration saine sur chacun de vos serveurs (ce qui peut être long et douloureux).
Maintenant que le serveur est complétement configuré, nous
arrivons à un classique problème d'oeuf et de poule, nous avons
besoin du update.conf pour récupérer la configuration qui
contient le fichier update.conf. Il faudra donc pour la première
fois le copier à la main sur le client dans le répertoire
/var/cfengine/inputs/ et lancer cfagent
:
# cfagent -K -q -v
Et miracle de l'informatique :
[...] Checking copy from 172.16.235.1:/var/cfengine/inputs/ to /var/cfengine/inputs Connect to 172.16.235.1 = 172.16.235.1 on port 5308 Updating last-seen time for 172.16.235.1 Loaded /var/lib/cfengine2/ppkeys/root-172.16.235.1.pub ............................................................... cfengine:cfclient: Strong authentication of server=172.16.235.1 connection confirmed cfengine:cfclient: INFO: /var/cfengine/inputs is a symbolic link, not a true directory! cfengine:cfclient: /var/cfengine/inputs/cfagent.conf wasn't at destination (copying) cfengine:cfclient: Copying from 172.16.235.1:/var/cfengine/inputs/cfagent.conf cfengine:cfclient: Object /var/cfengine/inputs/cfagent.conf had permission 600, changed it to 640 cfengine:cfclient: Update of image /var/cfengine/inputs/update.conf from master /var/cfengine/inputs/update.conf on 172.16.235.1 [...]
Vous avez diffusé votre première configuration cfengine.
Comme l'indique le début de la sortie de cfagent, le client et le
serveur ont échangé leur clé publique. Si vous êtes sur un
réseau pas ou peu sur vous pouvez maintenant commenter, sur le
serveur, les lignes TrustKeysFrom
dans cfservd.conf
et
trustkey
dans update.conf
sur le serveur. La prise en compte par
cfservd
est immédiate. Le client sera mis à jour à la
prochaine synchronisation. Quand vous voudrez ajouter un nouveau
client, vous devrez décommenter ces lignes le temps de la
première synchronisation.
Une fois cela fait, il ne vous restera plus qu'à ajouter une
tâche dans /etc/crontab pour lancer cfagent
périodiquement. Dans un premier temps, vous pouvez le lancer en
mode verbeux pour étudier ce qu'il fait :
# cat /etc/crontab 11 * * * * root /var/cfengine/bin/cfagent -v >> /tmp/cfagent.log 2>&1
Nous n'en parlerons pas plus avant mais Cfengine inclus un programme
spécial cfexecd
qui encapsule cfagent et permet d'envoyer un
mail avec la sortie de cfagent
. Vous pouvez l'utiliser à la
place de cfagent
dans votre crontab.
Jusqu'à maintenant, nous avons vu comment était structuré un fichier
de configuration cfengine, comment définir des classes et comment
lancer des commandes localement sur un serveur. Tout cela est bien
mais nous sommes loin de ce que nous avions annoncé dans
l'introduction de cet article, comme quoi cfengine pouvait, entre
autre, remplacer rsync
et NFS réunis.
Nous allons réutiliser telquel les fichiers cfservd.conf et <update.conf> que nous avons déjà vu.
Le fichier cfagent.conf ressemblera beaucoup à update.conf
et
ne fera qu'inclure des fichiers extérieurs :
# cat /var/cfengine/inputs/cfagent.conf control: actionsequence = ( copy files editfiles links shellcommands ) domain = ( mon.tld ) policyhost = ( 172.16.235.1 ) import: any:: cf.commun redhat:: cf.redhat
cfagent
ira chercher ces fichiers dans le répertoire par défaut.
Les noms de fichier de la forme « cf.* » sont une convention de nommage implicite mais généralement respectée pour les fichiers importés.
Vous pouvez inclure dans ce fichier cfagent.conf les exemples que nous allons voir ensemble ci-dessous et les étendre au fur et à mesure de vos besoins.
Pour vous permettre de tester en direct les exemples, j'ai ajouté une directives « actionsequence » au début de chacun. Il suffira donc de les recopier et de les lancer avec la commande :
cfagent -f cf.exemple
Pour que vous n'ayez pas à tout retaper, les exemples suivants sont disponibles sur le site des mongueurs perl : http://articles.mongueurs.net/magazines/cfengine/.
Plutôt que de faire à la main ce que nous avons vu plus haut, à savoir créer les liens vers /var/cfengine et ajouter cfagent dans le crontab, nous pouvons faire un script qui s'en chargera au premier lancement.
# cat /var/cfengine/inputs/cf.cfengine control: actionsequence = ( links editfiles ) editfiles: # attention à l'espace avant F</etc/crontab> linux:: { /etc/crontab SetLine "11 20 * * * root /usr/sbin/cfagent -v > /tmp/cfagent.log 2>&1 " AppendIfNoLineMatching "^.*cfagent.*$" } # les unix en général ne supportent pas que l'on spécifie # le propriétaire du programme à lancer !linux:: { /etc/crontab SetLine "11 20 * * * /usr/sbin/cfagent -v > /tmp/cfagent.log 2>&1" AppendIfNoLineMatching "^.*cfagent.*$" } links: debian:: /var/cfengine -> /var/lib/cfengine2 mandrake:: /var/cfengine -> /var/lib/cfengine !debian:: /etc/cfengine -> /var/cfengine/inputs/
# cat /var/cfengine/inputs/cf.menage controle: actionsequence = ( tidy ) # on supprime les fichiers de /tmp et /var/tmp de plus de 14 jours tidy: Sunday.Hr03:: /tmp pattern=* age=14 /var/tmp pattern=* age=14 # et les fichiers « indésirables » dans /home/ /home/ pattern=core R=inf age=1 /home/ pattern=*~ R=inf age=7 /home/ pattern=#* R=inf age=30
editfiles
# cat /var/cfengine/inputs/cf.editfiles control: actionsequence = ( editfiles shellcommands ) AddInstallable = ( sysctl ) # Cfengine refuse d'éditer les fichiers au dessus d'une # certaine taille et la limite est souvent un peu basse editfilesize = ( 30000 ) classes: # on définit une classe postgres s'il est installé postgres = ( IsDir(/usr/lib/postgresql/) ) editfiles: # on ajoute cfengine dans les services si il n'y est pas # déjà any:: { /etc/services SetLine "cfengine 5308/tcp # cfengine port" # on ajoute la ligne précédente si l'expression # rationnelle suivante n'est pas trouvée dans le fichier AppendIfNoLineMatching "^cfengine.*" } # on augmente shmall et shmmax pour les serveurs postgresql postgres:: { /etc/sysctl.conf # on ajoute ces lignes si elles n'existent pas déjà AppendIfNoSuchLine "kernel.shmall = 134217728" AppendIfNoSuchLine "kernel.shmmax = 134217728" # et on définit la classe sysctl DefineClasses "sysctl" } shellcommands: # si sysctl est défini alors on recharge /etc/sysctl.conf sysctl:: "/sbin/sysctl -p"
Dans ce script nous avons utilisé vu deux nouvelles directives sur lesquels nous devons nous arrêter un instant :
DefineClasses
elle permet de définir une classe dynamique si une
condition est remplie dans une directive editfiles
. Dans notre
exemple, si le fichier ne contient pas les lignes kernel.shmmax et
kernel.shmall, la classes sysctl
sera créée et la commande
sysctl -p
sera lancé dans la section shellcommands
suivante.
AddInstallable
le problème de définir une classe à la volée est que comme
cfengine parcourt ses fichiers en une passe, si une classe est
utilisée dans une section précédant celle où elle est
définie, elle ne sera pas vue par cfengine. Pour éviter cela il
faut le prévenir que cette classe existe, le mot clef
AddInstallable
sert à cela : prévenir cfengine que la classe
peut être définie quelque part. De manière générale, il
est plus sur de déclarer toutes vos classes dynamiques dans
AddInstallable
, cela vous évitera des surprises un jour.
# cat /var/cfengine/inputs/cf.dmz control: actionsequence = ( disable shellcommands files ) # cette base permettra de stocker les clés de hashage md5 des # fichiers qui sont calculées plus loin ChecksumDatabase = ( /var/cfengine/md5sum.db ) disable: /etc/hosts.equiv # upgrade de la distribution debian : # on préfère avoir un problème de mise à jour qu'un problème de # piratage shellcommands: debian.(Hr01|Hr12):: "/usr/bin/apt-get update" "/usr/bin/apt-get upgrade -y" filters: { root_owned_files Owner: "root" Mode: "+4000" Type: "file" Result: "Owner.Mode.Type" } files: # pour éviter les fausses manoeuvres, j'ai ajouté une classe # fictive "FAUX". Je m'en voudrais de saccager votre distribution. debian.FAUX:: # on supprime le bit setuid de tous les fichiers respectant le # filtre... ou presque / filter=root_owned_files mode=u-s action=fixall inform=true recurse=inf ignore=login ignore=passwd ignore=sudo ignore=visudo ignore=at # any:: /bin checksum=md5 r=inf /sbin checksum=md5 r=inf /usr/bin checksum=md5 r=inf /usr/sbin checksum=md5 r=inf /etc/ checksum=md5 r=inf
ATTENTION, si ces mesures, assez drastiques, peuvent se justifier sur un serveur en DMZ, il y a peu de chance que votre machine de bureau apprécie.
Arrêtons nous un instant sur filters
: ce mot-clé permet de
créer des filtres personnalisés utilisables entre autre dans la
section files
mais aussi dans la section processes
. Le filtre
est relativement parlant, Owner
, Mode
et Type
définissent
des conditions, Result
, définit comment combiner ces
conditions. La combinaison de condition est faite comme pour les
classes avec les opérateurs « et » (.
) et « ou » (|
ou ||
). La précédence est la même. Le filtre
défini cherche donc tous les fichiers appartenants à root avec
le bit setuid positionné.
Il sera sans doute appelé à partir d'un script du style :
control: actionsequence = ( copy shellcommands ) classes: # ajouter dans la liste le nom des serveurs apache apacheserveurs = ( azote plomb soufre ) import: debian.apacheserveurs:: cf.apache2
Et ressemblera à cela :
# cat /var/cfengine/inputs/cf.apache control: AddInstallable = ( apache2_start apache2_restart ) actionsequence = ( shellcommands ) serveur_maitre = ( 172.16.235.1 ) classes: # la classe est définie si le programme /usr/sbin/apache2 existe apache2 = ( FileExists("/usr/sbin/apache2") ) copy: # on copie la configuration à partir du serveur de référence # apt-get n'écrase pas une configuration existante /etc/apache2 dest=/etc/apache2/ server=$(serveur_maitre) r=inf define=apache2_restart # on copie le root directory de apache à partir du serveur référence /var/www/ dest=/var/www r=inf server=$(serveur_maitre) shellcommands: # on installe apache2 si on ne l'a pas trouvé !apache2:: "/usr/bin/apt-get install -y apache2" define=apache2_start # apres l'installation on demarre apache2_start:: "/etc/init.d/apache2 start" # si apache2_start et apache2_restart sont tous les deux # configurés on ne fait que le start apache2_restart.!apache2_start:: "/etc/init.d/apache2 restart"
Le mot clé define
agit comme le DefineClasses
vu plus haut.
Avec ce script vous pouvez très facilement ajouter des frontaux web en
ajoutant leur nom dans la liste apacheserveurs
.
Plutôt que de tester l'existence du fichier /usr/sbin/apache2
pour vérifier que Apache est installé, nous aurions pu utiliser
l'action packages
. Néanmoins, celle-ci ne fonctionnant que sur
les Unix utilisant rpm
, deb
et sur Sun, je préfère
l'éviter.
Dans cet exemple le but est de diffuser à moindre effort des modules Perl vers plusieurs architectures. Le principal problème est que, si les parties en perl sont compatibles d'un OS à l'autre, les parties en C ne le sont pas, donc qu'il n'est pas possible de copier toute l'arborescence d'un seul serveur maître. L'idée est d'avoir un serveur de référence par type d'OS nécessaire. La procédure de diffusion des modules Perl sera alors d'installer les modules Perl une fois sur chaque serveur de référence et d'utiliser le script ci-dessous pour diffuser les modules Perl.
On peut étendre ceci à la diffusion des programmes installés localement (dans /usr/local/bin) :
control: actionsequence = ( copy ) debian_ref = ( debian1.mon.tld ) aix_ref = ( aix1.mon.tld ) sco_ref = ( sco1.mon.tld ) copy: debian:: /usr/lib/perl5/ dest=/usr/lib/perl5/ server=$(debian_ref) r=inf /usr/local/bin dest=/usr/local/bin server=$(debian_ref) r=inf aix:: /usr/local/lib/perl/5.8.7/ dest=/usr/local/lib/perl/5.8.7/ server=$(aix_ref) r=inf /usr/local/bin dest=/usr/local/bin server=$(aix_ref) r=inf sco:: /usr/local/lib/perl/5.8.7 dest=/usr/local/lib/perl/5.8.7/ server=$(sco_ref) r=inf /usr/local/bin dest=/usr/local/bin server=$(sco_ref) r=inf
À noter qu'en cas de problème sur le serveur maître d'un type d'OS, il suffit d'élire un nouveau serveur de référence.
cfengine est un outil très puissant. Tellement puissant que l'expression classique « se tirer une balle dans le pied » peut rapidement devenir « se tirer un obus dans le pied ». Pour éviter cela, le seul moyen est d'être le plus rigoureux possible et de suivre une méthodologie claire, ce qui est d'ailleurs toujours une bonne idée quand vous avez à administrer un parc de serveurs.
La première chose à bien comprendre est que cfengine n'est pas un langage de programmation classique, c'est un langage descriptif. On décrit l'état que l'on veut obtenir et cfengine s'occupe de parvenir à cet état et de le maintenir.
Une conséquence de ceci est que vous devez tout décrire. Si vous êtes
en environnement hétérogène, vous avez intérêt à essayer de faire
converger ces environnements au maximum (c'est une bonne idée tant
pour cfengine que pour les administrateurs d'ailleurs). Si, par
exemple, votre système place ses journaux dans /var/adm, il peut être
intéressant de faire un lien symbolique de /var/log vers ce /var/adm
(cfengine peut même le faire pour vous avec l'action links
). Moins
vous aurez de différences entre vos systèmes, plus les règles seront
faciles à écrire (ou décrire) et moins les administrateurs seront
perturbés.
La première règle est de bien connaître votre configuration. Où est-elle stockée ? Quel serveur réplique quel serveur ?.
Utilisez un serveur maître unique. Ce serveur détiendra toute la configuration de cfengine. Il peut très facilement être dupliqué pour assurer de la haute disponibilité. Vous pouvez même prévoir dans vos scripts cfengine une bascule automatique sur un serveur de secours si le serveur maître ne répond pas. En plus de ce serveur maître général, il peut être utile de faire que chaque classe importante de serveurs dispose d'un serveur de référence.
Par exemple, tous les serveurs AIX pourront aller chercher les fichiers dépendant de AIX sur le serveur de référence AIX.
Au vu des possibilités de cfengine, il peut être vu comme un cron centralisé. Si ça n'est pas forcément une mauvaise idée dans la mesure où ça permet d'avoir une vision unifiée de tous vos jobs, cela n'est pas judicieux pour toutes les tâches. À vous de faire la part des choses.
N'essayez pas de tout faire dès le début. Commencez par de petites choses (vérifier l'existence de quelques répertoires, quelques droits de fichiers, ...), et enrichissez doucement vos scripts. Vous ferez moins d'erreurs, et vous vous apercevrez que le plus difficile n'est pas de le décrire en cfengine mais de savoir ce que vous faites précisément. Vous entrerez alors dans un processus incrémental d'amélioration de la qualité de votre travail et de libération de temps : ce qui est décrit dans cfengine va progressivement se normaliser sur votre parc, et vous n'aurez plus à vous en soucier. Quant au petit nouveau de votre équipe, il trouvera une partie de la documentation dans vos scripts.
Utilisez CVS ou tout autre système de gestion de source pour conserver une trace de toutes vos modifications. Cette recommandation est d'autant plus importante si vous êtes plusieurs à modifier la configuration de cfengine. Travaillez sur une copie des fichiers dans un répertoire personnel et une fois les modifications faites et commitées, mettez à jour le répertoire de configuration de Cfengine.
Vous pouvez même faire valider par CVS vos modifications avant de les mettre en ligne.
Voici un exemple du script utilisé chez nous :
$ cat $CVSROOT/CVSROOT/cfcommit.pl #!/usr/bin/perl use strict; use warnings; # the cfengine program my $cfengine = '/usr/sbin/cfagent'; # The list of error we want to search my $error = 0; for (@ARGV) { # we just test files next unless -f $_; my $res = system("$cfengine -pf ./$_"); if ($res) { print "Erreur dans le fichier $_\n"; $error = 1; } } exit $error;
Pour le mettre en place, faite un checkout de votre répertoire CVSROOT :
cvs co CVSROOT cd CVSROOT # copiez le fichier cfcommit.pl dans ce répertoire cp ~/cfcommit.pl . # ajoutez ce fichier dans la liste des fichiers gérés par CVS echo cfcommit.pl >> checkoutlist echo '^cf /usr/bin/perl $CVSROOT/CVSROOT/cfcommit.pl "%s"' >> commitinfo cvs add cfcommit.pl cvs ci -m "ajout de la verification des cf.*"
Comme vous utilisez CVS, utilisez les tags (ou équivalent) pour gérer
vos versions. Quand une version des fichiers est prête à être diffusée
en production, tagguez-la PROD
. Ainsi vous pourrez utiliser la
commande suivante pour mettre à jour sans crainte vos fichiers
cfengine :
cvs co -r tag PROD
Il n'y aura plus aucun risque de voir une version non testée arriver en production par erreur.
Adoptez une politique de diffusion claire et compréhensible.
Vous pouvez par exemple conserver tous les fichiers à synchroniser
(autre que la configuration de cfengine) dans une arborescence commune
et ajouter une ligne dans update.conf
pour la récupérer en
une seule fois. Vous pourrez ensuite utiliser cette copie locale dans
votre script cfagent.conf
.
En bonus vous pourrez ajouter cette arborescence dans l'outil de gestion de version et assurer une meilleure traçabilité.
Si certains répertoires sont trop gros pour être répliqués
comme cela ou bien utilisés sur quelques serveurs seulement
(fichiers statiques html, journaux, ...), n'hésitez pas à les
recopier directement à la source. Rappelez-vous, pour cela, que
tous les serveurs peuvent servir à diffuser des fichiers du moment
qu'un démon cfservd
est lancé.
Pratiquer la « revue par les pairs » : faire lire vos modifications par vos collègues avant de les passer en production.
Testez toujours vos fichiers avec l'option --dry-run
et en
utilisant la possibilité de définir des classes par l'option -D
.
Testez toujours (TOUJOURS) vos modifications sur un serveur de test avant de les diffuser en production. Si vous avez des classes vraiment différentes, l'idéal est bien sûr d'avoir un serveur de test dans chaque classe, même si cela coûte cher. Cela coûtera peut-être moins cher que de devoir réinstaller tous vos serveurs...
Une bonne méthode est de faire vos modifications sur un serveur de test avant de les valider puis de les passer en production. Comme cela vous êtes sûr que vos modifications ne rentreront pas en production sans que vous le vouliez. Si vous n'avez pas de serveur libre, utilisez Xen ou un autre outil de virtualisation pour vous en faire un.
L'organisation des fichiers est importante, essayez de faire des fichiers regroupant des fonctions du même registre. Ne mélangez pas tout. Ne laissez pas vos fichiers grossir, scindez-les en actions plus petites et utilisez import pour les assembler. Par exemple :
cf.postgres : tout ce qui a trait aux serveurs Oracle cf.apache : les serveurs web cf.cron : les tâches récurrentes sur tous les serveurs cf.site : les procédures valables pour tous vos serveurs cf.aix : les procédures liées à AIX cf.linux : les procédures Linux génériques cf.debian : les tâches liées à Debian cf.redhat : ... cf.ftp : les procédures pour installer un nouveau serveur FTP
Cette organisation a des limites, elle peut devenir trop grosse ou
incompréhensible à cause des effets de bords. N'hésitez pas
à la découper en plusieurs configurations totalement distinctes
pour Apache, PostgreSQL, ... et lancez-les séparément avec f-
:
# cfagent # cfagent -f postgres.conf # cfagent -f apache.conf
De façon à bien différencier du premier coup d'oeil les
fichiers importés par la commande import
des fichiers de
configuration principaux, je préfère postfixer ces derniers d'un
« .conf ».
Le langage de cfengine a beau être puissant, il est parfois peu adapté à vos besoins. Dans ces cas là il vaut mieux revenir à votre langage de prédilection pour faire un programme que vous lancerez depuis cfengine.
Essayez d'intégrer cfengine le plus tôt possible dans votre
architecture. Par exemple, plutôt que de faire toutes les petites
tâches post-installation de vos serveurs à la main, faites les
faire par cfengine. C'est d'ailleurs ce qui est fait par l'outil
d'installation automatique de debian FAI
http://www.informatik.uni-koeln.de/fai/
Tout ce qui est faisable de manière automatique hors de cfengine est réalisable dans cfengine. Mais ce n'est pas forcément pour cela qu'il faut le faire avec cfengine. Si vous savez installer un logiciel sur votre système de manière automatisée, mais que votre fournisseur vous retire le support si vous passez par autre chose que son CD, continuez avec le CD et discutez avec votre fournisseur pour le convaincre d'assouplir sa position. Ou alors débrouillez-vous sans support (mais je ne suis pas sur que votre employeur voit les choses de la même manière que vous).
La directive editfiles
peut se révéler complexe,
réservez-la à la modification de fichiers partagés par
plusieurs produits logiciels. Par exemple, chaque processus qui
utilise un port devrait se déclarer dans /etc/services mais vous
ne mettez pas forcément tous les outils sur chaque machine. Donc
soit vous avez un fichier /etc/services complet (mais comment
ferez-vous le jour où un nouveau logiciel va arriver avec un
nouveau port ?), soit vous faites en sorte que chaque outil ajoute ce
qui le concerne dans ce fichier. Pensez à ce que ça peut donner
avec des fichiers de clefs SSH, ou des fichiers d'utilisateurs,
tcpwrappers, ... En revanche, si votre but est de modifier un fichier
qui n'est utilisé que par un seul outil (sshd_config par exemple ne
sert qu'à sshd), et sur tous vos serveurs de la même façon,
conservez ce fichier sur le serveur maître et diffusez-le par
copy
. Si vous avez une version de fichier par OS ou par classes,
conservez-en un de chaque en les suffixant (sudoers.aix,
sudoers.debian, ...) et utilisez une configuration du type :
control: cfconfdir = ( /usr/local/cfengine/conf ) copy: aix:: $(cfconfdir)/etc/sudoers.aix dest=/etc/sudoers server=serveur_maitre linux:: $(cfconfdir)/etc/sudoers.linux dest=/etc/sudoers server=serveur_maitre !(aix|linux):: $(cfconfdir)/etc/sudoers dest=/etc/sudoers server=serveur_maitre
Une deuxième solution est de classer vos fichiers par OS ou type d'OS :
copy: aix:: $(cfconfdir)/OS/AIX/etc/sudoers dest=/etc/sudoers server=serveur_maitre linux:: $(cfconfdir)/OS/LINUX/etc/sudoers dest=/etc/sudoers server=serveur_maitre [...]
Choisissez la solution qui vous convient le mieux et surtout, tenez-vous y.
Si, malgré ces conseils pratiques, vous diffusez une configuration cfengine erronée, ne paniquez pas, vous pouvez utiliser le fichier update.conf pour rediffuser une version saine.
Et même comme ça, préparez-vous au pire, la puissance d'un outil se paye toujours un jour ou l'autre.
Un jour, un collègue a eu besoin de créer une classe dynamique à partir du nom des serveurs. Il cherchait à exécuter une action sur tous les serveurs ayant un nom contenant un caractère « - ». Il a donc fait quelque chose comme cela :
classes: classe_dynamique = ('/bin/hostname | /bin/grep -- "-"' )
Là où cela devient intéressant c'est que pour exécuter cette ligne de
commande cfengine utilise la fonction execv()
. Cette fonction ne
fonctionne pas du tout comme un shell mais prend la première partie
comme commande et lui passe en argument tout ce qui se trouve à sa
droite. Elle va donc passer le caractère « | » comme paramètre à
hostname
qui, dans ce cas, va fixer le nom du serveur au paramètre
passé. Imaginez un parc de plusieurs dizaines de serveurs répondant
tout d'un coup au doux nom de « | ». Si vous n'arrivez pas à
imaginer c'est normal mais sachez que les serveurs Apache, les bases
de données (MySQL, PostgreSQL, Oracle, ...), les serveurs DNS et en
général tout ce qui a ou fait du réseau supporte très mal cela et,
sous l'outrage, préfère arrêter de fonctionner.
Tout ceci pour dire que cfengine ne doit surtout pas vous empêcher de préparer des procédures de secours, de vérifier vos sauvegardes et de tester vos restaurations régulièrement.
Nous avons fini notre tour d'horizon de Cfengine. Vous aurez sans doute constaté que cet outil a un coût d'entrée non négligeable. Comme tous les outils puissants il faut se l'approprier avant de pouvoir pleinement l'utiliser, mais une fois maîtrisé, il permet une efficacité difficile à atteindre sans automatisation. D'autant que cet article n'a fait qu'effleurer ses possibilités.
Commencez petit, faites faire à cfengine toutes les petites tâches répétitives de nettoyage et de peaufinage et montez en puissance au fur et à mesure. Mais n'oubliez surtout pas la règle cardinale : testez vos modifications avant de les passer en production, sans quoi la sanction peut être lourde.
[1] Essential System Administration 3ed, Æleen Frisch. O'Reilly, 2002.
[2] un wiki
[3] cfengine
[4] Projet EPEL, Extra Packages for Enterprise Linux http://fedoraproject.org/wiki/EPEL et http://download.fedora.redhat.com/pub/epel/
[5] documentation cfengine http://www.cfengine.org/docs/cfengine-Reference.html
Nicolas Chuche - <nchuche@barna.be>
Nicolas Chuche est ingénieur système au ministère de l'Équipement et utilisateur de systèmes GNU/Linux et Unix depuis une dizaine d'années.
Merci à Patrice Guerlais pour ses modifications et ses conseils avisés de vieux routard de Cfengine et aux Mongueurs de toute la Francophonie qui ont assuré la relecture de cet article.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.