Table of Contents

Lundi - Introduction

Navigation rapide : Lundi / Mardi / Mercredi / Jeudi / Vendredi Mémos : Perl / Python / Ruby

TP - Parseur de dhcpd.conf

Objectif

Le but du TP est de récupérer des informations contenues dans un fichier dhcpd.conf. Ce fichier ressemble à ça :

#
# DHCP Server Configuration file.
#   see /usr/share/doc/dhcp*/dhcpd.conf.sample
#
ddns-update-style ad-hoc;
not authoritative;
min-lease-time 43200;
max-lease-time 43200;

subnet 172.19.45.0 netmask 255.255.255.0 {
        deny unknown-clients ;
        range 172.19.45.245 172.19.45.254;
        option subnet-mask              255.255.0.0;
        option domain-name-servers      172.19.45.18,172.19.45.20;
        option routers  172.19.0.254;
}

host coquil-gw1 { hardware ethernet 00:1d:09:0a:33:db; fixed-address coquil-gw1; }
host fpl { hardware ethernet 00:26:b9:5d:32:32; fixed-address fpl; }
host roundcube
{
  hardware ethernet 00:16:36:09:2d:b9; fixed-address roundcube;
}
host deux { hardware ethernet 00:14:38:63:6E:F3; fixed-address deux; }
host visiteur1 { hardware ethernet 00:00:aa:be:93:5d; }
host visiteur2 { hardware ethernet 00:60:B0:D5:3B:05; }

les sauts de lignes ne sont pas significatifs : en particulier, les déclarations host peuvent s'étendre sur plusieurs lignes, et inversement une ligne peut en contenir plusieurs

Voici·un·véritable·fichier·dhcpd.conf

Sujet

Nous allons écrire un script qui parcours un fichier dhcpd.conf, et construit une structure de données contenant pour chaque host une liste d'attributs choisis, associés à leur valeur. Ci-dessous un exemple de la structure de données que l'on pourrait construire :

'fpl'       => {
                  'ip' => 'fpl',
                  'mac' => '00:26:b9:5d:32:32'
               },
'roundcube' => {
                  'ip' => 'roundcube',
                  'mac' => '00:16:36:09:2d:b9'
               },
'visiteur2' => {
                  'mac' => '00:60:B0:D5:3B:05'
               }

Etape 1

Question

Comme le formatage du fichier dhcpd.conf est très libre par rapport aux sauts de lignes, on va reformater le fichier à notre façon. Pour cela, on va fusionner toutes les lignes en une grande string, que l'on pourra ensuite parser.

Il s'agit donc ici :

Réponse

bash

 1: #!/bin/bash
 2:
 3: # Les structures de données sont très limités en bash. Ici
 4: # nous utiliserons plutot un fichier intermédiaire qu'une liste
 5: # comme cela a été en perl, ruby et python.
 6:
 7: # Nom du fichier tampon
 8: FILETMP=/tmp/dhcpd-tmp.$$
 9:
10: # Reformattage du fichier dhcpd.conf passé sur stdin :
11: # - grep -v pour enlever les lignes vides
12: # - sed pour retirer les commentaires
13: # - tr -d pour enlever les sauts de lignes (doit être fait en dernier)
14: grep -v '^$' < /dev/stdin | sed -e 's/#.*//g' | tr -d '\n' > $FILETMP
15:
16: cat $FILETMP
17: rm $FILETMP

perl

 1: use FileHandle ;
 2: use Data::Dumper ;
 3:
 4: use strict ;
 5:
 6: # mettre le contenu du fichier dans une string
 7: my $lines ;
 8: while (<>)
 9: {
10:   chomp ;       # supprime le \n final
11:   s/#.*// ;     # supprime les commentaires
12:   next unless /\S/ ; # si la ligne est vide, au suivant !
13:
14:   $lines .= $_ ;   # $_ est la ligne courant
15: }
16:
17: # découper la string pour en extraire les portions host {}
18: my @lines = $lines=~/(host\s.*?})/g ;
19:
20: print Dumper \@lines ;

[Python]
Mots clefs: re.findall

python

 1: #!/usr/bin/python
 2: import fileinput
 3: import re
 4:
 5: # on concatène toutes les lignes du fichier en supprimant les commentaires et les fins de ligne
 6: lines = ''
 7: for line in fileinput.input():
 8:     lines = lines + re.sub('#.*','',line.rstrip())
 9:
10: # on extrait toutes les occurences de "host ...}"
11: hosts = re.findall('host\s[^}]*}',lines)
12:
13: # et on affiche la liste obtenue
14: print hosts

[Ruby]
Mots clefs: File.readlines(), Array.collect!(), String.chomp!(), String.match(), Array.compact(), Array.join(), String.scan()

ruby

require 'pp'
 
lines  = File.readlines('dhcpd.conf')        # lis le fichier ligne par ligne
stream = lines.collect! do |line|            # remplace chaque ligne
        line.chomp!                          # supprime le \n final
        (line.match(/#/).nil?) ? line : nil; # remplace les commentaire par nil
end.compact.join('')                         # supprime les nil et refait une string
hosts  = stream.scan(/host\s*[^}]+\}/)       # identifie les blocs "host"
pp hosts                                     # affiche la liste

Etape 2

Question

Parser la liste précédente pour en extraire le nom du host et la valeurs des attributs hardware ethernet et fixed-address. Pour chaque host, créer un petit hash dont les clés sont ”hardware ethernet” et ”fixed-address”, et dont les valeurs sont les valeurs de ces attributs.

Réponse

perl

 1: use FileHandle ;
 2: use Data::Dumper ;
 3:
 4: use strict ;
 5:
 6: my $lines ;
 7: while (<>)
 8: {
 9:   chomp ;
10:   s/#.*// ;
11:   next unless /\S/ ;
12:
13:   $lines .= $_ ;
14: }
15:
16: my @lines = $lines=~/(host\s.*?})/g ;
17:
18: for my $l (@lines)
19: {
20:   my $item ;                      # pointeur de hash pour les attributs du host courant
21:
22:   my ($host,$rest) = $l=~/host\s*(\S+)\s*\{\s*(.*?)\s*\}/ ;
23:
24:   if ($rest=~/hardware\s+ethernet\s+\S+\s*;/)
25:   {
26:     ($item->{mac}) = $rest=~/hardware\s+ethernet\s+(\S+)\s*;/ ;   # on extrait
27:     $rest =~ s/hardware\s+ethernet\s+\S+\s*;// ;                  # on efface
28:   }
29:
30:   if ($rest=~/fixed-address\s+\S+\s*;/)
31:   {
32:     ($item->{ip}) = $rest=~/fixed-address\s+(\S+)\s*;/ ;          # on extrait
33:     $rest =~ s/fixed-address\s+\S+\s*;// ;                        # on efface
34:   }
35:
36:   print Dumper $item ;
37: }

python

 1: #!/usr/bin/python
 2: import fileinput
 3: import re
 4:
 5: lines = ''
 6: for line in fileinput.input():
 7:     lines = lines + re.sub('#.*','',line.rstrip())
 8:
 9: hosts = re.findall('host\s[^}]*}',lines)
10:
11: # on prépare une liste de regexps
12: regexps=[
13:     re.compile('(fixed-address)\s+(\S+)'),
14:     re.compile('(hardware\sethernet)\s+(\S+)')
15:     ]
16:
17: # on boucle sur les hosts
18: for host in hosts:
19:     # un dictionnaire vide **par host** qu'on va remplir
20:     attributes={}
21:     m=re.match('host\s+(\S+)\s*{\s*(.*?)\s*}',host)
22:     if m:
23:         # on récupère d'une part le hostname, d'autre part ses attributs
24:         hostname,context=m.group(1),m.group(2)
25:         # les attributs sont séparés par des ;
26:         # on boucle sur les champs
27:         for field in re.split(';\s*',context):
28:             # pour chaque regexp, on teste si le champ matche
29:             for pattern in regexps:
30:                 n=pattern.search(field)
31:                 if n:
32:                     # si c'est le cas, n.group(1) contient l'étiquette (par ex: fixed-address)
33:                     # n.group(2) contient la valeur (par ex: 00:16:36:09:2d:b9)
34:                     attributes[n.group(1)]=n.group(2)
35:         # on affiche l'attribut
36:     # on est revenu au niveau de la première boucle for
37:     print attributes


Mots clefs: String.split(), Array.each()

ruby

regexp = /host\s*(\S+)\s*\{\s*(.*?)\s*\}/
pp hosts.collect do |host|                                        # remplace le tableau de string
        attributes = {}                                           # par un tableau de hash
        host.match(regexp)                                        # extrait les attributs
        hostname,context=$1,$2                                    #
        fields = context.split(';')                               # sépare les attributs sur ';'
        fields.each do |field|                                    #
                [ '(hardware\s+ethernet)\s+(\S+)',                # identifie les attributs
                  '(fixed-address)\s+(\S+)'                       #
                ].each do |pattern|
                        attributes[$1] = $2 unless field.chomp.match(pattern).nil?
               end
        end
        attributes
end

Etape 3

Question

Pour mémoriser l'ensemble des infos nous intéressant, on va créer un grand hash dont les clés sont les noms de host, et dont les les valeurs sont les petits hashs précédents. En fin de programme, faire un dump de ce grand hash.

Réponse

perl

 1: use FileHandle ;
 2: use Data::Dumper ;
 3:
 4: use strict ;
 5:
 6: my $lines ;
 7: while (<>)
 8: {
 9:   chomp ;
10:   s/#.*// ;
11:   next unless /\S/ ;
12:
13:   $lines .= $_ ;
14: }
15:
16: my @lines = $lines=~/(host\s.*?})/g ;
17: my $info ;            # pointera vers le hash pour stocker les attributs de tous les hosts
18: for my $l (@lines)
19: {
20:   my $item ;
21:
22:   my ($host,$rest) = $l=~/host\s*(\S+)\s*\{\s*(.*?)\s*\}/ ;
23:
24:   if ($rest=~/hardware\s+ethernet\s+\S+\s*;/)
25:   {
26:     ($item->{mac}) = $rest=~/hardware\s+ethernet\s+(\S+)\s*;/ ;
27:     $rest =~ s/hardware\s+ethernet\s+\S+\s*;// ;
28:   }
29:
30:   if ($rest=~/fixed-address\s+\S+\s*;/)
31:   {
32:     ($item->{ip}) = $rest=~/fixed-address\s+(\S+)\s*;/ ;
33:     $rest =~ s/fixed-address\s+\S+\s*;// ;
34:   }
35:
36:   $info->{$host} = $item ;       # nouvel host
37: }
38:
39: print Dumper $info ;

python

 1: #!/usr/bin/python
 2: import fileinput
 3: import re
 4: import pprint
 5:
 6: lines = ''
 7: for line in fileinput.input():
 8:     lines = lines + re.sub('#.*','',line.rstrip())
 9:
10: hosts = re.findall('host\s[^}]*}',lines)
11: # un dictionnaire vide qui contiendra les valeurs pour chaque host
12: hosts_hash = {}
13: regexps=[
14:     re.compile('(fixed-address)\s+(\S+)'),
15:     re.compile('(hardware\sethernet)\s+(\S+)')
16:     ]
17: for host in hosts:
18:     attributes={}
19:     m=re.match('host\s+(\S+)\s*{\s*(.*?)\s*}',host)
20:     if m:
21:         hostname,context=m.group(1),m.group(2)
22:         for field in re.split(';\s*',context):
23:             for pattern in regexps:
24:                 n=pattern.search(field)
25:                 if n:
26:                     attributes[n.group(1)]=n.group(2)
27:         # on stocke les dictionnaire attributes dans le dictionnaire hosts_hash sous la clé hostname
28:         hosts_hash[hostname] = attributes
29: pprint.pprint(hosts_hash)


Mots clefs: Hash[]

ruby

hosts_array = hosts.collect do |host|
        attributes = {}
        host.match(regexp)
        hostname,context=$1,$2
        fields = context.split(';')
        fields.each do |field|
                [ '(hardware\s+ethernet)\s+(\S+)',
                  '(fixed-address)\s+(\S+)'
                ].each do |pattern|
                        attributes[$1] = $2 unless field.chomp.match(pattern).nil?
               end
        end
        [ hostname, attributes ]
end
pp Hash[*hosts_array.flatten]     # la notation *hosts_array eclate le tableau d'objet
                                  # en autant de paramètres à passer à la méthode de classe Hash[].
pp Hash[hosts_array]              # mais cela marche aussi

Etape 4

Question

Pour rendre le programme plus extensible, on va définir dans une structure de données, les attributs à récupérer, et pour chacun d'eux, l'expression régulière permettant de l'extraire. Par la suite, il suffira donc d'ajouter un nouvel élément dans ce hash pour prendre en compte de nouveaux attributs.

Créer un hash dont les clés sont les noms des attributs à récupérer, et dont les valeurs sont les expressions régulières permettant de les récupérer. Transformer le programme afin d'extraire les attributs spécifiés dans la structure précédente.

Sortir le grand hash à l'écran, lisiblement, sans dump.

Réponse

perl

 1: use FileHandle ;
 2: use Data::Dumper ;
 3:
 4: use strict ;
 5:
 6: # ce qu'il faut extraire, et comment le faire
 7: my %attributes =
 8:   (
 9:     mac => qr/hardware\s+ethernet\s+(\S+?)\s*;/
10:     , ip => qr/fixed-address\s+(\S+?)\s*;/
11:   ) ;
12:
13: my $lines ;
14: while (<>)
15: {
16:   chomp ;
17:   s/#.*// ;
18:   next unless /\S/ ;
19:
20:   $lines .= $_ ;
21: }
22:
23: my @lines = $lines=~/(host\s.*?})/g ;
24: my $info ;
25: for my $l (@lines)
26: {
27:   my $item ;
28:
29:   my ($host,$rest) = $l=~/host\s*(\S+)\s*\{\s*(.*?)\s*\}/ ;
30:
31:   # recherche des attributs
32:   my ($key,$re) ;
33:   while (($key,$re)=each %attributes)
34:   {
35:     if ($rest=~$re)
36:     {
37:       ($item->{$key}) = $rest=~/$re/ ;
38:       $rest =~ s/$re// ;
39:     }
40:   }
41:
42:   $info->{$host} = $item ;                                        # nouvel host
43: }
44:
45: # parcours du hash
46: while (my($host,$attr)=each %$info)
47: {
48:   print "\n$host:\n" ;
49:   for my $attr_name (keys %$attr)
50:   {
51:     print "\t$attr_name: ",$attr->{$attr_name},"\n" ;
52:   }
53: }

python

 1: #!/usr/bin/python
 2: import fileinput
 3: import re
 4:
 5: # un dictionnaire qui contient les regexp que nous allons utiliser
 6: attributes_match={
 7:     'mac':'(hardware\sethernet)\s+(\S+)',
 8:     'ip':'(fixed-address)\s+(\S+)'
 9:     }
10:
11: lines = ''
12: for line in fileinput.input():
13:     lines = lines + re.sub('#.*','',line.rstrip())
14:
15: hosts = re.findall('host\s[^}]*}',lines)
16: hosts_hash = {}
17:
18: for host in hosts:
19:     attributes={}
20:     m=re.match('host\s+(\S+)\s*{\s*(.*?)\s*}',host)
21:     if m:
22:         hostname,context=m.group(1),m.group(2)
23:         for field in re.split(';\s*',context):
24:             # iteritems permet de boucler sur clé,valeur
25:             for key,pattern in attributes_match.iteritems():
26:                 n=re.search(pattern,field)
27:                 if n:
28:                     # ici key peut valoir 'mac' et n.group(2) peut valoir '00:16:36:09:2d:b9'
29:                     attributes[key] = n.group(2)
30:         hosts_hash[hostname] = attributes
31:
32: # iteritems permet de boucler sur clé,valeur
33: for hostname,attributes in hosts_hash.iteritems():
34:     print "%s:"%hostname
35:     for name,value in attributes.iteritems():
36:         print "\t%s: %s"%(name,value)


Mots clefs: Hash.each()

ruby

attributes_match = {
        'mac' => '(hardware\s+ethernet)\s+(\S+)',
        'ip'  => '(fixed-address)\s+(\S+)'
}
 
hosts_array = hosts.collect do |host|
        attributes = {}
        host.match(regexp)
        hostname,context=$1,$2
        fields = context.split(';')
        fields.each do |field|
                attributes_match.each do |key,pattern|
                        attributes[key] = $2 unless field.chomp.match(pattern).nil?
               end
        end
        [ hostname, attributes ]
end
hosts_hash = Hash[*hosts_array.flatten]
hosts_hash.each do |hostname,attributes|
        puts "#{hostname}:"
        attributes.each do |name,value|
                puts "\t#{name}: #{value}"
        end
end

Version

$Id: dhcpd.conf.txt 683 2012-05-28 19:57:45Z jaclin $