[couverture de Linux Magazine 95]

cfengine - un outil pour l'administrateur système

Article publié dans Linux Magazine 95, juin 2007.

Copyright © 2007 - Nicolas Chuche

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

Introduction

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 - un outil pour l'administrateur système

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

Installation de Cfengine

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.

Les systèmes de packages

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

Compilation

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.

Description et principes de fonctionnement

Présentation de l'architecture

La liste des programmes

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.

Les répertoires de cfengine

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 :

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

Les fichiers de configuration se trouvent donc normalement dans /var/cfengine/inputs. Voici les principaux :

Un fichier de configuration très simple

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.

La notion de classes

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 :

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]

Syntaxe des fichiers de configuration

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.

Listes des sections utilisables

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.

Les sections implicites

Les sections explicites

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.

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.

Configuration du réseau

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.

Configuration de 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.

Configuration de l'agent

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

Comment initialiser la procédure

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.

Quelques exemples d'utilisation

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

achever l'installation de 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/

Un script s'occupant des tâches ménagères

  # 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

Un exemple d'utilisation de la directive 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 :

Un script s'occupant de sécuriser un minimum les serveurs en DMZ

  # 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é.

Un script s'occupant de déployer Apache 2

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.

Diffusion de modules Perl multi-architecture.

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.

Un peu de méthodologie

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.

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.

Conclusion

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.

Liens et livres

Ours

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.

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]