Article publié dans Linux Magazine 119, septembre 2009.
Copyright © 2008 - Dominique Dumont
Config::Model
Dans un article précédant [GLMF], nous avons vu comment créer la partie
graphique d'un éditeur de configuration en précisant la structure et
les contraintes des données du fichier
/etc/ssh/sshd_config. Config::Model
va utiliser cette structure (le
modèle de la configuration de sshd_config) pour générer l'interface
graphique. Mais il reste à pouvoir charger les données du fichier et
les ré-écrire. Nous allons voir dans cette deuxième partie comment utiliser
l'API de Config::Model
pour lire et écrire les données de sshd_config.
Malgré toutes les relectures, une erreur s'est glissée dans la
première partie de cet article. Cette erreur enlève tout intérêt à
l'utilisation du paramètre compute
de Config::Model::Value
.
Dans le cas de X11Forwarding
situé dans une section Match
,
compute
sert à déterminer la valeur par défaut à montrer à
l'utilisateur. Cette valeur est calculée à partir du paramètre
X11Forwarding
situé dans la section principale de sshd_config
.
Dans le paragraphe 5.16, en bas à gauche de la page 84, le 3e élément
de la liste est faux. La valeur par défaut indiquée par l'éditeur de
sshd_config
pour X11Forwarding
est «yes
» (et non pas «no
»)
dans l'instance de Sshd::Elements
si X11Forwarding
est «yes
»
dans l'instance de Sshd
.
L'auteur vous présente ses excuses pour cette erreur.
Config::Model
fournit quelques modules pour charger des fichiers au
format INI ou des structures de données Perl.
La syntaxe du fichier sshd_config parait simple, mais ses
conventions adoptées ne permettent pas d'utiliser le module de
chargement des fichier INI. En effet, il faut traiter spécialement les
paramètres comme Match
.
Donc il va falloir écrire notre propre lecteur de sshd_config. On
va créer la classe Perl Config::Model::Sshd
qui contiendra une
méthode read
et une méthode write
. Dans la documentation de
Config::Model
, on les mentionne sous le nom de parser et
writer.
Cette section va permettre d'aborder l'API de Config::Model
.
Mais d'abord, il va falloir renseigner le modèle de configuration de
façon à ce que Config::Model
sache quelle classe et méthode
utiliser pour lire le ficher sshd_config :
Dans l'éditeur du modèle de configuration, ouvrez en édition le
paramètre read_config
de la classe Sshd
. Ce paramètre est une
liste de nœuds de classe Itself::ConfigWR
. Cette classe est
imposée et vous ne pouvez pas en changer.
Cliquez sur Push new node (Itself::ConfigWR
) pour créer un lecteur
dans le modèle.
Ouvrez le paramètre read_config
et la première instance 0
.
Assignez le paramètre backend
à custom
. Vous verrez apparaître deux
nouveaux paramètres qu'il faudra aussi renseigner.
Assignez Config::Model::Sshd
au paramètre class
et read
au
paramètre function
.
Assignez /etc/ssh au paramètre config_dir
pour indiquer où se
trouvent les fichiers de configuration.
Faites presque pareil pour le paramètre write_config
en assignant
write
au paramètre function
.
Ce qui donne ce modèle :
'read_config' => [ { 'function' => 'read', 'class' => 'Config::Model::Sshd', 'backend' => 'custom' } ], 'write_config' => [ { 'function' => 'write', 'class' => 'Config::Model::Sshd', 'backend' => 'custom' } ],
Maintenant, il va falloir coder ces fonctions avec votre éditeur favori.
Config::Model
pour charger sshd_configLes méthodes indiquées plus haut vont être appelées avec les paramètres suivants :
( object => ... , config_dir => ... )
Le paramètre object
étant un objet de type Config::Model::Node
et config_dir
le répertoire où lire les fichiers de configuration.
Et voici le code de lecture (expurgé du traitements des erreurs pour
limiter sa taille). On commence par la fonction déclarée dans le
paramètre read_config
du modèle.
package Config::Model::Sshd ; sub read { my %args = @_ ; # On retrouve les deux paramètres d'appel "object" et "config_dir" my $config_root = $args{object} ; my $dir = $args{config_dir} ; # Jusqu'ici, rien de bien sorcier. Maintenant on va lire le fichier # sshd_config et le nettoyer un peu : my $file = "$dir/sshd_config" ; my $fh = new IO::File $file, "r" ; my @file = $fh->getlines ; $fh->close; # On supprime les commentaires et les espaces en début de ligne map { s/#.*//; s/^\s+//; } @file ; parse ( join('',@file), $config_root ) ; }
Maintenant, on attaque les choses sérieuses. Le lecteur se base sur
Parse::RecDescent
. C'est peut-être un marteau-pilon pour écraser
une mouche, mais ça permet de traiter rapidement le fait que les
arguments de sshd_config peuvent être entre guillemets.
$parser
est une variable globale qui va contenir l'analyseur créé
par Parse::RecDescent
. La méthode sshd_parse
va être créée par
Parse::RecDescent
à partir de la grammaire déclarée un peu plus
loin. Notez que cette grammaire ne va pas traiter les erreurs. Les
erreurs seront détectées par Config::Model
.
Pour pouvoir enregistrer les données du fichier sshd_config, il
faut passer l'objet config::Model::Node
qui contient l'arbre de
configuration :
sub parse { my ( $text, $config_root) = @_ ; $parser->sshd_parse($text, # text to be parsed 1, # start $config_root # arguments ) ; }
Et voici la grammaire en question. Le [@arg]
sert à propager
l'argument passé à sshd_parse
. Comme déboguer du code Perl embarqué
dans les actions de Parse::RecDescent
est vite pénible, on saute le
plus vite possible dans une fonction dédiée.
$grammar = << 'EOG' ; # Voir la FAQ de Parse::RecDescent à propos des lignes finissant par \n sshd_parse: <skip: qr/[^\S\n]*/> line[@arg](s) # Les lignes commençant par «Match» doivent être traitées # spécialement car elles définissent un bloc qui se fini à la # prochaine ligne «Match» ou à la fin du fichier. # Et les lignes commençant par «ClientAlive» doivent aussi être # traitées spécialement car il faut assigner 1 au «warp master» # «ClientAliveCheck» avant de pouvoir assigner la valeur de # «ClientAliveInterval» ou «ClientAliveCountMax» line: match_line | client_alive_line | any_line # Et voici le traitement de la ligne «Match». $arg[0] contient # $config_root et $item[2] est une référence sur une liste qui # contient tous les argument de la ligne «Match». match_line: /match/i arg(s) "\n" { # action: on saute vite dans une fonction dédiée Config::Model::Sshd::match($arg[0],@{$item[2]}) ; } # Voici le traitement des lignes «ClientAliveInterval» # ou «ClientAliveCountMax» client_alive_line: /clientalive\w+/i arg(s) "\n" { Config::Model::Sshd::clientalive($arg[0],$item[1],@{$item[2]}) ; } # traitement générique pour toutes les autre lignes any_line: key arg(s) "\n" { Config::Model::Sshd::assign($arg[0],$item[1],@{$item[2]}) ; } key: /\w+/ arg: string | /\S+/ string: '"' /[^"]+/ '"' EOG
Cette instruction sert à compiler la grammaire en phase de démarrage :
$parser = Parse::RecDescent->new($grammar) ;
Maintenant, on attaque la partie ou les informations extraites par
Parse::RecDescent
sont chargées dans Config::Model
. Dans la
plupart des cas, il faudra charger les informations dans la racine du
modèle. Mais, à l'intérieur d'un bloc Match
, il faudra charger les
informations dans un autre nœud de l'arbre. Donc, à chaque ligne, il
faut savoir si on doit utiliser la racine ($root
) ou un autre
nœud. C'est la variable lexicale $current_node
qui va garder cette
information.
my $current_node ; # pour savoir quel noeud charger
La fonction assign
est appelée pour chaque ligne en dehors des
lignes Match
et ClientAlive*
. Une des particularité de syntaxe de
sshd_config
est de ne pas être sensible à la casse pour les mots
clefs. Donc, si le mot clef trouvé par Parse::RecDescent
est
inconnu, il faut chercher le bon élément du modèle parmi tous ceux
disponibles :
sub assign { my ($root, $key,@arg) = @_ ; # initialise current_node pour le 1er appel $current_node = $root unless defined $current_node ; # Si on ne trouve pas l'élément... if ( not $current_node->element_exists( $key ) ) { # ...on cherche parmi tous ceux disponibles... foreach my $elt ($current_node->get_element_name(for => 'master') ) { # ... celui qui correspond sans tenir compte de la casse. $key = $elt if lc($key) eq lc($elt) ; } } # On récupère l'élément de l'arbre avec fetch_element() my $elt = $current_node->fetch_element($key) ; # on récupère le type de l'élément pour savoir comment le traiter my $type = $elt->get_type; # on stocke l'information extraite de sshd_config : if ($type eq 'leaf') { $elt->store( $arg[0] ) ; # classe Config::Model::Value } elsif ($type eq 'list') { $elt->push ( @arg ) ; # classe Config::Model::ListId } elsif ($type eq 'hash') { # classe Config::Model::HashId. On récupère l'objet # Config::Model::Value et puis on stocke l'information $elt->fetch_with_id($arg[0])->store( $arg[1] ); } elsif ($type eq 'check_list') { # classe Config::Model::CheckList. my @check = split /,/,$arg[0] ; $elt->set_checked_list (@check) ; } else { # Comme on dit, «ça ne devrait jamais arriver» die "Sshd::assign did not expect $type for $key\n"; } }
La fonction match
est appelée chaque fois qu'une ligne Match
est
trouvée dans sshd_config.
sub match { my ($root, @pairs) = @_ ; # classe Config::Model::ListId my $list_obj = $root->fetch_element('Match'); # Il s'agit maintenant de créer un nouveau noeud my $nb_of_elt = $list_obj->fetch_size; # fetch_with_id va auto-vivifier un nouveau noeud # Sshd::MatchBlock. Notez que $block_obj est un objet de classe # Perl Config::Model::Node my $block_obj = $list_obj->fetch_with_id($nb_of_elt) ; while (@pairs) { my $criteria = shift @pairs; # critère sshd_config my $pattern = shift @pairs; # motif sshd_config # load() permet d'utiliser une notation compacte plus pratique # que d'invoquer fetch_with_id() et store() # Voir Config::Model::Loader pour plus de détails $block_obj->load(qq!$criteria="$pattern"!); } # Maintenant, on crée un nouvel objet de config Sshd::MatchElement # (en Perl c'est un Config::Model::Node) et on le stocke dans # $current_node pour que tous les lignes restantes de sshd_config # soient enregistrées dans le block «Match» correspondant. $current_node = $block_obj->fetch_element('Elements'); }
La fonction clientalive
est appelée chaque fois qu'une ligne
ClientAliveCountMax
ou ClientAliveInterval
est trouvée dans
sshd_config.
sub clientalive { my ($root, $key, $arg) = @_ ; # avec cette instruction qui renseigne le paramètre «artificiel», # les éléments ClientAliveInterval et ClientAliveCountMax passent # de «hidden» à «normal» grâce au mécanisme de «warping» # $root->load("ClientAliveCheck=1") ; # Maintenant on peut faire le traitement habituel assign($root,$key,$arg) ; }
Et voilà, c'est fini. Le lecteur de fichier sshd_config est complet.
Je ne vais pas trop rentrer dans les détails car cette partie est plus simple. Il s'agit simplement d'explorer le contenu de l'arbre de configuration et de sauvegarder les valeurs différentes des valeurs par défaut.
La fonction write
déclarée dans le modèle va avoir la même
signature que la fonction read
avec les deux paramètres d'appel
object et config_dir.
Dans la fonction principale, il faut interroger l'arbre de configuration pour avoir tous les éléments définis :
# le paramètre "master" permet d'avoir tous les paramètres foreach my $name ($node->get_element_name(for => 'master') ) { # saute ceux qui n'ont pas été utilisés next unless $node->is_element_defined($name) ; # récupère l'élément my $elt = $node->fetch_element($name) ; my $type = $elt->get_type; if ($name eq 'Match') { $match .= write_all_match_block($elt) ; } elsif ($type eq 'leaf') { # récupère la valeur contenue dans l'arbre my $v = $elt->fetch ; if (defined $v and $elt->value_type eq 'boolean') { # sshd ne comprend pas 1 ou 0 $v = $v == 1 ? 'yes':'no' ; } $result .= "$name $v\n" if defined $v; } elsif ($type eq 'check_list') { # récupère la valeur contenue dans l'arbre my $v = $elt->fetch ; $result .= "$name $v\n" if $v; } elsif ($type eq 'list') { map { $result .= "$name $_ \n" ;} $elt->fetch_all_values ; } elsif ($type eq 'hash') { foreach my $k ( $elt->get_all_indexes ) { my $v = $elt->fetch_with_id($k)->fetch ; $result .= "$name $k $v\n"; } } else { die "Sshd::write did not expect $type for $name\n"; } }
Le corps de la fonction write_all_match_block
va explorer tous les
blocs Match
disponibles :
foreach my $elt ($match_list->fetch_all() ) { $result .= write_match_block($elt) ."\n"; }
Et la fonction write_match_block
va explorer les quatre éléments
User
, Group
, Host
et Address
foreach my $name ($match_bloc->get_element_name(for => 'master') ) { my $elt = $match_elt->fetch_element($name) ; if ($name eq 'Elements') { # l'appel à write_node_content est récursif $result .= "\n".write_node_content($elt)."\n" ; } else { my $v = $elt->fetch($name) ; $result .= "$name $v " if defined $v; } }
L'approche choisie par Config::Model
pour lire et écrire le fichier
de configuration sshd_config a pour principale limitation de ne
pas tenir compte des commentaires du fichier. Ceux-ci ne sont pas lus
et ne peuvent être restitués quand le fichier sshd_config est
écrit.
Malheureusement, l'interface de configuration de sshd est encore
bien intimidante pour un administrateur système débutant : en
lançant config-edit -model Sshd
, il découvre une bonne cinquantaine
de paramètres. Config::Model
offre plusieurs possibilités pour
limiter le nombre de paramètres exposés à l'utilisateur.
Certains des paramètres de sshd_config s'adressent plutôt à des
experts. Par exemple, le paramètre MACs, qui spécifie les
algorithmes d'authentifications disponibles en protocole SSH v2 est
certainement destiné au utilisateurs avertis. En terme de modèle, le
paramètre experience
est réglé à master
.
Une fois que tous les éléments de Sshd
et Sshd::MatchElement
sont ainsi classifiés, l'utilisateur pourra utiliser le menu
Options/experience
pour se concentrer sur les paramètres les plus
intéressants pour lui.
Ce réglage de niveau d'expérience sur tous les paramètres de sshd_config ne sera sans doute au point qu'après de nombreux commentaires des utilisateurs. Heureusement, la mise au point du modèle de sshd_config sur ce point ne demandera que des modifications très rapides du modèle de sshd_config.
Actuellement, plusieurs interfaces utilisateurs sont disponibles. À vous de prendre celle qui convient le mieux à votre environnement.
Une interface graphique basée sur Perl/Tk. (Malheureusement, le « wizard » tant vanté en début d'article n'est pas encore prêt)
Une interface Curses basée sur Curses::UI
(avec son
« wizard »). Cette interface est pratique si votre serveur Xorg est cassé
ou si vous devez intervenir sur la machine de votre belle-maman à
travers ADSL. (C'est du vécu ;-) )
Une interface en ligne basée sur Term::ReadLine
avec
auto-complétion des commandes possibles.
Une interface bête et méchante (car elle ne vous aide pas du tout) qui
prends des commandes sur STDIN
et renvoie les résultats sur
STDOUT
. Cette interface est plutôt destinée à être utilisée à
partir d'un autre programme.
Une interface en ligne de commande pour pouvoir scripter les actions de
configuration à travers Config::Model
:
$ config-edit -model Sshd -ui none MaxStartups=10:40:80
Notez que ces interfaces sont générées à partir du modèle de sshd_config et qu'elles s'adapteront automatiquement à toutes ses évolutions.
Elles seront aussi disponibles pour tous les autres modèles qui seront
créés pour Config::Model
.
Depuis la parution de la première partie de cet article, le paramètre
built_in
a été renommé en upstream_default
.
config-edit
(fournit par Config::Model::Itself
) peut être
utilisé pour mettre à jour les modèles d'une manière automatique avec
cette commande :
config-edit -model Sshd -ui none -save
Sans cette migration, vous verrez passer quelques avertissements sans gravité.
Config::Model
Config::Model
s'applique bien aux configurations (relativement)
simples comme sshd_config ou aux configurations assez complexes
comme xorg.conf ou fstab. Mais certaines configurations comme celle
d'Exim faisant appel à des variables et à des instructions
conditionnelles ne pourront sans doute pas être modélisées.
Un des objectifs de Config::Model
est de fédérer les configurations
de projet au-delà de sshd. Mais pour y parvenir, il y aura un
certain nombres d'obstacles à franchir :
Limiter la duplication d'information entre les projets et les modèles de configuration. Dans le cas de sshd_config, toutes les descriptions des éléments ont été extraites de la page de manuel du projet OpenSSH. Il faudra veiller à mettre à jour ces descriptions en fonction des changements fait pour les prochaines versions d'OpenSSH. C'est une tâche qui sera vite rébarbative et qui ne sera pas gérable avec des projets évoluant plus vite.
Pouvoir fournir les descriptions en plusieurs langues. Ce qui rendra les problèmes de synchronisation entre les projets et les modèles de configuration encore plus délicats à gérer.
Préserver les commentaires des administrateur systèmes. Les
commentaires des fichiers systèmes contiennent souvent des
informations précieuses pour les administrateurs. Pouvoir les
préserver serait un atout non négligeable. Une possibilité est
d'utiliser le projet Augeas [AUGEAS] qui sait préserver les
commentaires. Ceci est en cours de développement avec
Config::Model::Backend::Augeas
.
Pouvoir installer ou enlever des portions d'un modèle sans tout
casser. Prenons l'exemple de la configuration de Xorg, il faudrait
pouvoir installer ou enlever le modèle de configuration de la carte
NVidia sans casser le modèle Xorg lui-même. En d'autres termes, il
faut un mécanisme de greffons pour Config::Model
.
Mieux tester et expliquer comment gérer les mises-à-jour de
configuration lors des mises à jour de logiciels. En effet, la mise à
jour des configurations est souvent beaucoup plus délicate à gérer que
la configuration lors de la première installation. Config::Model
fournit quelques mécanismes pour gérer ces mises à jour (status
deprecated
ou obsolete
pour les éléments, paramètre replace
pour
les types énumérés, utilisation de compute
pour convertir des
valeurs d'un ancien élément vers un nouveau), mais il faut encore
trouver des cas réels pour valider ces mécanismes. A ma connaissance,
aucun système ne gère correctement les mises à jour. C'est pourtant un
point fondamental et c'est une des priorités du projet
Config::Model
.
On peut aussi imaginer une interface Web à la Webmin (ou pouvoir
appeler Config::Model
à partir de Webmin [WEBMIN])
Ça fait beaucoup de boulot pour une personne seule ... (et hop, encore un petit appel du pied pour les volontaires)
Il n'y a aucune raison technique qui empêche d'intégrer
Config::Model
dans les systèmes de configuration de Debian
(debconf
) ou même de Fedora (system-config-*
). Ce sera plus
difficile dans le cas de Fedora car il faudra pouvoir appeler du code
Perl à partir de code Python, mais c'est faisable avec PyPerl [PYPERL]
qui fournit un module Python pour appeler du Perl.
Mais il faudra que Config::Model
et ses modèles fassent leur
preuves avant que les différents responsables de nos distributions
favorites considèrent un changement radical de leurs outils de
configuration.
Si vous voulez :
faciliter l'adoption de votre projet favori en fournissant une interface de configuration graphique,
ou limiter la prolifération des fichiers .rpmsave et .rpmnew dans votre répertoire /etc,
ou ne plus contempler d'un air dubitatif les diff contextuels
présentés par debconf
lors d'une mise à jour,
Vous pouvez proposer votre aide :
sur la liste de développement du projet Config::Model
[CMDEVEL]
pour participer au développement des interfaces ou de Config::Model
sur la liste des utilisateurs de Config::Model
[CMUSERS] pour
participer à l'élaboration de nouveaux modèles, à l'amélioration de la
documentation [SOURCEFORGE] ou tout simplement pour poser des
questions sur la façon d'utiliser Config::Model
pour votre propre
projet.
Les mongueurs de Perl pour leur accueil et la relecture de cet article.
Mes collègues de Hewlett-Packard pour leur soutien et les relectures.
Les pages du projet :
[GLMF] GNU/Linux Magazine/France numéro 117 page 72
[FRESHMEAT] http://freshmeat.net/projects/config_model/
[SOURCEFORGE] http://config-model.wiki.sourceforge.net/
[CPAN] http://search.cpan.org/~ddumont/
Les autres liens :
[AUGEAS] http://augeas.net/
[WEBMIN] http://www.webmin.com/
[PYPERL] http://wiki.python.org/moin/PyPerl
[PYTHON] http://search.cpan.org/dist/Inline-Python/
[CMDEVEL] http://lists.sourceforge.net/mailman/listinfo/config-model-devel
[CMUSERS] http://lists.sourceforge.net/mailman/listinfo/config-model-users
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.