Skip to content

Generate a configuration editor from a config template file with Perl: lcdproc example

July 3, 2011

Hello

Creating a configuration model for Config::Model can be a boring task: When a configuration system has a lot of parameters, a lot of details need to be specified to have a complete configuration validation tool. So, all tricks are permitted to make this task more bearable.

Let’s see how this can be applied to lcdproc. This project requires a configuration file that supports more than 40 drivers. Each driver has it own set of parameters for a total of about 250 parameters. That’s a lot to specify..

Fortunately, LCDd.conf template file is written in a way which makes it relatively easy to parse and get all required information to build a model.

All drivers are listed in this template file, most parameters have default values and legal values are written in comments in a uniform way. Hence this file (and comments) can be parsed to retrieve information required for the model.

This blog will describe how this task can be implemented in Perl using Config::Model modules. The code will be slightly trimmed down to keep this blog (relatively) short. All initialisation code and error checks are removed. You can get the whole script on github.

The main steps are:

  1. prepare a parser for LCDd.conf template
  2. parse LCDd.conf template
  3. prepare a LCDd model skeleton
  4. translate LCDd.conf template information in a format suitable to create a model.
  5. Write the resulting LCDd model

First, here’s the necessary incantation to start properly a Perl program:

use strict;
use warnings;

Then load all required libraries. Their use will be described later:

use Config::Model;
use Config::Model::Itself 1.225;
use Config::Model::Backend::IniFile;

use 5.10.0;
use IO::File;
use IO::String;

Perform some initialization to avoid upsetting the logging system:

use Log::Log4perl qw(:easy);
Log::Log4perl->easy_init($WARN);

Let’s also initialize a model object that will be very useful later:

my $model = Config::Model->new();

Now we can really start the fun part.

Step 1: Create a parser for LCDd.conf (INI file)

Problem: LCDd.conf comments must also be analysed to get more semantic information. The comments must be retrieved and associated with the INI classes and parameters found in this file.

Fortunately, Config::Model::Backend::IniFile is able to associate comments with the relevant parameters. But Config::Model::Backend::IniFile must store its values in a configuration tree. So let’s create a model suitable for LCDd.conf template that accepts any INI class and any INI parameter

Dummy::Class is used to store any parameter found in an INI class. The accept keyword with .* regular expression means that any INI parameter will be accepted in each INI class.

$model->create_config_class(
  name   => 'Dummy::Class',
  accept => [ 
    '.*' => { 
       type => 'leaf',
       value_type => 'uniline'
       }, 
    ],
);

Now the Dummy model must provide a configuration class to hold all INI classes:

$model->create_config_class(
    name   => 'Dummy',
    accept => [ 
       '.*' => { 
           type => 'node', 
           config_class_name => 'Dummy::Class' 
       }, 
    ],
);

Note that an INI backend could be specified directly here. But, some useful parameters are commented out in LCD.conf. Some some processing is required to be able to create a model with these commented parameters. See below for this processing.

Step 2: Parse LCDd.conf

Now the dummy configuration class is created. Let’s create a configuration tree from this dummy class to store the data from LCDd.conf

my $dummy = $model->instance(
    instance_name   => 'dummy',
    root_class_name => 'Dummy',
)-> config_root;

Now, read LCDd.conf:

my $lcd_file 
  = IO::File->new('examples/lcdproc/LCDd.conf');
my @lines    = $lcd_file->getlines;

And perform the pre-processing on LCDd.conf mentioned above. Just un-commenting commented parameters is required:

foreach (@lines) { s/^#(\w+=)/$1/ }

Store the pre-processed LCDd.conf in a IO::Handle usable by INI backend

my $ioh = IO::String->new( join( '', @lines ) );

Create an INI backend to read the data from pre-processed LCDd.conf and store it in the configuration tree:

my $ini_backend = Config::Model::Backend::IniFile
  ->new( node => $dummy );
$ini_backend->read( io_handle => $ioh );

Step 3: Prepare LCDd configuration model

Your attention please. You may remember that we invoked instance on object to create the “dummy” configuration tree to hold LCDd.conf data. The instance method used the Dummy configuration class.

Now, we must invoke instance again on object. But this time, instance will use Config::Model::Itself. This model is Config::Model’s way of eating its own dog food. The configuration tree created there will be able to hold LCDd model. Hence the root of this special configuration tree will be called “meta_root” to avoid confusion with the other configuration root defined above (for the “Dummy” model).

Still with me?

Good, let’s create this “meta” tree that will contain LCDd model:

my $meta_root = $model->instance(
    root_class_name => 'Itself::Model',
    instance_name   => 'meta_model',
) -> config_root;

Create the main LCDd configuration class and store the first comment from LCDd.conf as class description:

my $first_comment = $dummy->annotation ;
$meta_root
  ->grab("class:LCDd class_description")
  ->store( $first_comment );

Append my own text in this description:

my $extra_description =
    "Model information extracted from template /etc/LCDd.conf"
  . "\n\n=head1 BUGS\n\nThis model does not support to load "
  . "several drivers. Loading several drivers is probably a "
  . "marginal case. Please complain to the author if this "
  . "assumption is false";
$meta_root->load(
     qq!class:LCDd 
        class_description.="\n\n$extra_description"!
);

Add legal stuff in the model:

$meta_root->load( qq!
  class:LCDd 
    copyright:0="2011, Dominique Dumont" 
    copyright:1="1999-2011, William Ferrell and others" 
    license="GPL-2"
!
);

Add INI backend so configuration object created from LCDd model will be able to read user’s INI files:

$meta_root->load( qq!
  class:LCDd 
    read_config:0 
      backend=ini_file 
      config_dir="/etc" 
      file="LCDd.conf"
!
);

Note that all the load calls above could be done as one call. They were split for better clarity.

Step 4:Extracting LCDd.conf information

Now that all information from LCDd.conf template file is stored in $dummy configuration tree, it’s time to use this information to enhance the skeleton of LCDd model created above.

Let’s first find all the INI classes defined in LCDd.conf:

my @ini_classes = $dummy->get_element_name;

Now before actually mining LCDd.conf information, we must prepare several subs to handle them. This is done using a dispatch table:

my %dispatch;

First create the default entry which will be used for most parameters. This subs is passed: the INI class name, the INI parameter name, the comment attached to the parameter, the INI value, and an optional value type. It will return a string describing a model for simple elements. For instance:

class:"LCDd::MD8800" element:Device type=leaf value_type=uniline default="/dev/ttyS1" description="device to use [default: /dev/ttyS1]"

Here’s the subroutine invoked by default. Let’s create a an anonymous subroutine and store it in the dispatch table:

$dispatch{_default_} = sub {
    my ( $ini_class, $ini_param, $ini_note, 
         $ini_v, $value_type ) = @_;
    $value_type ||= 'uniline';

Now, prepare a string that will be used later to create the ini_class model. This string uses Config::Model’s serialization format described in Config::Model::Loader:

    my $load = qq!class:"$ini_class" 
                  element:$ini_param type=leaf !;

Get semantic information from LCDd.conf template comments (written between square brackets):

    my $info = $ini_note =~ /\[(.*)\]/ ? $1 : ''; 
    $info =~ s/\s+//g;

Use this semantic information to better specify the parameter:

    $load .=
        $info =~ /legal:(\d+)-(\d+)/ 
           ? " value_type=integer min=$1 max=$2"
      : $info =~ /legal:([\w\,]+)/ 
           ? " value_type=enum choice=$1"
      :    " value_type=$value_type";

    if ( $info =~ /default:(\w+)/ ) {
        # specify upstream default value 
        # if it was found in the comment
        $load .= qq! upstream_default="$1"! 
           if length($1);
    }
    else {
        # or use the value found in INI 
        # file as default
        $ini_v =~ s/^"//g;
        $ini_v =~ s/"$//g;
        $load .= qq! default="$ini_v"! 
           if length($ini_v);
    }

The model (in serialized format) is ready, let’s return it and exit the subroutine:

    
    return $load;
};

Now let’s take care of the special cases. This one deals with “Driver” parameter found in INI [server] class. This sub will create a string for an enum element. The possible values of the enum are extracted from the comment found in the LCDd.conf file:

$dispatch{"LCDd::server"}{Driver} = sub {
    my ( $class, $elt, $info, $ini_v ) = @_;
    my $load = qq!class:"$class" 
                    element:$elt 
                      type=leaf 
                      value_type=enum
    !;
    my @drivers = split /\W+/, $info;
    while ( @drivers 
            and ( shift @drivers ) !~ /supported/ ) {
      # nothing, the work is done by shift operator 
      # in loop test to skip all words in $info until
      # 'The following drivers are supported:'.
      # ok, that's a hack
    }
    $load .= 'choice=' 
          . join( ',', @drivers ) . ' ';

    return $load;
};

This sub invokes the default sub and add a small feature to the model to ensure that DriverPath ends with ‘/’. This is ensured by the match="/$" snippet:

$dispatch{"LCDd::server"}{DriverPath} = sub {
    my ( $class, $elt, $info, $ini_v ) = @_;
    return   $dispatch{_default_}->(@_) 
           . q! match="/$" !;
};

Likewise, this sub invokes the default sub with the optional 4th parameter that forces the value_type of the element. In this case, we enforce an integer element:

 $dispatch{"LCDd::server"}{WaitTime} 
 = $dispatch{"LCDd::server"}{ReportLevel} 
 = $dispatch{"LCDd::server"}{Port}  
 = sub {
    my ( $class, $elt, $info, $ini_v ) = @_;
    return $dispatch{_default_}->( @_, 'integer' );
  };

This sub returns a string for a list element with default values:

  $dispatch{"LCDd::server"}{GoodBye} 
= $dispatch{"LCDd::server"}{Hello} 
= sub {
    my ( $class, $elt, $info, $ini_v ) = @_;
    my $ret = qq( class:"$class" 
                    element:$elt 
                      type=list
                ) ;
    $ret .= 'cargo type=leaf 
             value_type=uniline - ' ;  
    $ret .= 'default_with_init:0="\" '.$elt.'\"" ' ; 
    $ret .= 'default_with_init:1="\" LCDproc!\""'; 
    return $ret ;
};

Now that the dispatch table is ready, let’s really mine LCDd.conf information.

First handle all INI classes in the foreach loop:

foreach my $ini_class (@ini_classes) {

Let’s retrieve parameters for this INI class from the LCDd.conf template:

  my $ini_obj = $dummy->grab($ini_class);

This will be the name of the lcdproc driver configuration class (e.g. LCDd::imonlcd):

  my $config_class   = "LCDd::$ini_class";

The configuration class will also be created in a serialized format. Let’s create config class in case there’s no parameter in INI file:

  $meta_root->load(qq!class:"LCDd::$ini_class"!);

Now, loop over all INI parameters and create LCDd::$ini_class elements:

  my @all_ini_params = $ini_obj->get_element_name ;  
  foreach my $ini_param ( @all_ini_params ) {

Retrieve the value provided in template LCDd.conf:

    my $ini_v = $ini_obj->grab_value($ini_param);

Retrieve the comment attached to the value provided in template LCDd.conf:

    my $ini_comment 
      = $ini_obj->grab($ini_param)->annotation;
    # escape embedded quotes
    $ini_comment =~ s/"/\\"/g;

Retrieve the correct sub from dispatch table:

    my $sub = $dispatch{$config_class}{$ini_param}
            || $dispatch{_default_};

Run the sub to get the model string:

    my $model_spec 
      = $sub->( $config_class, $ini_param, 
                   $ini_comment, $ini_v );

Attach the comment from LCDd.conf as class description:

    $model_spec .= qq! description="$ini_comment"!
      if length($ini_comment);

Then, last but not least, load class specification in the model, and end the loop aroung INI parameters:

    $meta_root->load($model_spec);
  }

Now create a an $ini_class element in LCDd class (to link LCDd class and LCDd::$ini_class):

  my $driver_class_spec = qq!
    class:LCDd 
      element:$ini_class 
  ! ;

Since server and menu are not drivers, they have a special treatment:

  if (   $ini_class eq 'server' 
      or $ini_class eq 'menu' ) {
    $driver_class_spec .= qq! 
      type=node 
      config_class_name="LCDd::$ini_class" 
    ! ;
  }

Here’s the treatment for LCDd drivers. Setup the model with a warped node so that a driver configuration class is shown only if the driver was selected in the [server] class:

  else {
    $driver_class_spec .= qq! 
      type=warped_node 
      config_class_name="LCDd::$ini_class"
      level=hidden
      follow:selected="- server Driver"
      rules:"\$selected eq '$ini_class'" 
        level=normal
    !;
  }

Now we can load the LCDd driver model in meta root:

  $meta_root->load($driver_class_spec);
}

Step 5: save LCDd model

Still with me ?

Good. Here’s the last part, now that the model was created in memory, let’s save it in files. Config::Model::Itself constructor returns an object to read or write the data:

my $rw_obj = Config::Model::Itself
     ->new( model_object => $meta_root );
say "Writing all models in file (please wait)";
$rw_obj->write_all( 
    model_dir => 'lib/Config/Model/models/'
);

That’s it. Once this script is run, the directory lib/Config/Model/models/ contains a bunch of files:

You can see the resulting editor in my first blog on lcdproc.

Conclusion and future steps (with you?)

As mentioned at the end of this blog, the result of this model generation process is far from perfect, because the information retrieved from LCDd.conf template are not complete for this task (which is normal since these comments were targeted for users).

Now, I’d really like to discuss with project developers to improve configuration template to generate better models.

For project owner, such an improvement would give them:

  • command lines to validate configuration
  • graphical configuration editor with integrated help
  • Configuration doc generated from the information stored in the template file

Please leave a comment on this blog (or send a message to config-model-users at lists.sourceforge.net) if you are interested in configuration tool generation for a project you contribute to.

All the best

[ Note: updated links and corrected some typos. Added mention of pod doc ]

About these ads

From → Config::Model, Perl

One Comment

Trackbacks & Pingbacks

  1. New configuration editor for LCDPROC « Ddumont's Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: