Erratum
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.
1. Comment lire et écrire le fichier de configuration
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.
1.1 Utilisation de l'API de Config::Model pour charger sshd_config
Les 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.
1.2 Sauvegarde des données de sshd_config
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";
}
}
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;
}
}
1.3 Limitations de l'éditeur de configuration sshd_config
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.
1.4 Classification en terme d'expérience
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.
Les interfaces disponibles pour l'éditeur 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.
3. Evolution
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é.
4. Limitations de 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.
5. Les prochains chantiers
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 projetAugeas [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::Modelfournit 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)
6. Oui, bon, et ma distribution Linux favorite alors ?
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.
7. Mais que peut-on faire alors ?
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.
Remerciements
- 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.
Liens
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