Config::Model - Créer un éditeur graphique de configuration avec Perl (2e partie)

Article publié dans Linux Magazine 119, septembre 2009.

Copyright © 2008 - Dominique Dumont

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

Chapeau

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.

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.

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 :

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.

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.

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";
            }
        }
        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;
        }
    }

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.

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.

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.

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

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.

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 :

Ça fait beaucoup de boulot pour une personne seule ... (et hop, encore un petit appel du pied pour les volontaires)

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.

Mais que peut-on faire alors ?

Si vous voulez :

Vous pouvez proposer votre aide :

Remerciements

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

Auteur

Dominique Dumont (dominique.dumont@hp.com)

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