source: trunk/klask @ 19

Last change on this file since 19 was 19, checked in by g7moreau, 16 years ago
  • Damned, forget to try before commit ! I forgot a semi comma...
  • Property svn:executable set to *
File size: 39.8 KB
Line 
1#!/usr/bin/perl -w
2
3use strict;
4use warnings;
5
6use Net::SNMP;
7use YAML qw(:all);
8use Net::Netmask;
9use Net::CIDR::Lite;
10use NetAddr::IP;
11use Getopt::Long;
12
13# apt-get install snmp fping libnet-cidr-lite-perl libnet-netmask-perl libnet-snmp-perl libnetaddr-ip-perl libyaml-perl
14# libcrypt-des-perl libcrypt-hcesha-perl   libdigest-hmac-perl
15
16my $KLASK_DB_FILE  = '/var/cache/klask/klaskdb';
17my $KLASK_SW_FILE  = '/var/cache/klask/switchdb';
18my $KLASK_CFG_FILE = '/etc/klask.conf';
19
20my $KLASK_CFG = YAML::LoadFile("$KLASK_CFG_FILE");
21
22my %DEFAULT = %{$KLASK_CFG->{default}};
23my @SWITCH  = @{$KLASK_CFG->{switch}};
24
25my %switch_level = ();
26LEVEL_OF_EACH_SWITCH:
27for my $sw (@SWITCH){
28   $switch_level{$sw->{hostname}} = $sw->{level} || $DEFAULT{switch_level}  || 2;
29   }
30@SWITCH = sort { $switch_level{$b->{hostname}} <=> $switch_level{$a->{hostname}} } @{$KLASK_CFG->{switch}}; 
31
32my %SWITCH_PORT_COUNT = ();
33
34my %CMD_DB = (
35   help       => \&cmd_help,
36   exportdb   => \&cmd_exportdb,
37   updatedb   => \&cmd_updatedb,
38   searchdb   => \&cmd_searchdb,
39   removedb   => \&cmd_removedb,
40   search     => \&cmd_search,
41   enable     => \&cmd_enable,
42   disable    => \&cmd_disable,
43   status     => \&cmd_status,
44   updatesw   => \&cmd_updatesw,
45   exportsw   => \&cmd_exportsw,
46   dotsw      => \&cmd_exportsw_dot,
47   iplocation => \&cmd_iplocation,
48   );
49
50my %INTERNAL_PORT_MAP = (
51   0 => 'A',
52   1 => 'B',
53   2 => 'C',
54   3 => 'D',
55   4 => 'E',
56   5 => 'F',
57   6 => 'G',
58   7 => 'H',
59   );
60my %INTERNAL_PORT_MAP_REV = reverse %INTERNAL_PORT_MAP;
61
62my %SWITCH_CODE = (
63   J4120A   => 'HP J4120A ProCurve Switch 1600M',
64   J4093A   => 'HP J4093A ProCurve Switch 2424M',
65   J4813A   => 'HP J4813A ProCurve Switch 2524',
66   J4900A   => 'HP J4900A ProCurve Switch 2626',
67   J9021A   => 'ProCurve J9021A Switch 2810-24G',
68   J4903A   => 'HP J4903A ProCurve Switch 2824',
69   J4110A   => 'HP J4110A ProCurve Switch 8000M',
70   );
71
72my %SWITCH_TYPE = (
73   J4120A   => 'HP1600M  ',
74   J4093A   => 'HP2424M  ',
75   J4813A   => 'HP2524   ',
76   J4900A   => 'HP2626   ',
77   J9021A   => 'HP2810-24',
78   J4903A   => 'HP2824   ',
79   J4110A   => 'HP8000M  ',
80   );
81
82my %OID_NUMBER = (
83   sysDescr    => '1.3.6.1.2.1.1.1.0',
84   sysName     => '1.3.6.1.2.1.1.5.0',
85   sysContact  => '1.3.6.1.2.1.1.4.0',
86   sysLocation => '1.3.6.1.2.1.1.6.0',
87   );
88
89################
90# principal
91################
92
93my $cmd = shift @ARGV || 'help';
94if (defined $CMD_DB{$cmd}) {
95   $CMD_DB{$cmd}->(@ARGV);
96   }
97else {
98   print STDERR "klask: command $cmd not found\n\n";
99   $CMD_DB{help}->();
100   exit 1;
101   }
102
103exit;
104
105###
106# fast ping dont l'objectif est de remplir la table arp de la machine
107sub fastping {
108   system "fping -c 1 @_ >/dev/null 2>&1";
109   }
110
111###
112# donne l'@ ip, dns, arp en fonction du dns OU de l'ip
113sub resolve_ip_arp_host {
114   my $param_ip_or_host = shift;
115   my $interface = shift || '*';
116   my $type      = shift || 'fast';
117
118   my %ret = (
119      hostname_fq  => 'unknow',
120      ipv4_address => '0.0.0.0',
121      mac_address  => 'unknow',
122      );
123
124#   my $cmdarping  = `arping -c 1 -w 1 -rR $param 2>/dev/null`;
125
126   # controler que arpwatch tourne !
127   # resultat de la commande arpwatch
128   # /var/lib/arpwatch/arp.dat
129   # 0:13:d3:e1:92:d0        192.168.24.109  1163681980      theo8sv109
130   #my $cmd = "grep  -e '".'\b'."$param_ip_or_host".'\b'."' /var/lib/arpwatch/arp.dat | sort +2rn | head -1";
131#   my $cmd = "grep  -he '".'\b'."$param_ip_or_host".'\b'."' /var/lib/arpwatch/*.dat | sort +2rn | head -1";
132   my $cmd = "grep  -he '".'\b'."$param_ip_or_host".'\b'."' /var/lib/arpwatch/$interface.dat | sort +2rn | head -1";
133   my $cmd_arpwatch = `$cmd`;
134   chomp $cmd_arpwatch;
135   my ($arp, $ip, $timestamp, $host) = split /\s+/, $cmd_arpwatch;
136#print "OOO $cmd\n";
137#print "TTT arp $arp -> $ip pour host $host\n";
138   $ret{ipv4_address} = $ip        if $ip;
139   $ret{mac_address}  = $arp       if $arp;
140   $ret{timestamp}    = $timestamp if $timestamp;
141
142   my $nowtimestamp = time();
143
144   if ( $type eq 'fast' and ( not defined $timestamp or $timestamp < ( $nowtimestamp - 3 * 3600 ) ) ) {
145      $ret{mac_address} = 'unknow';
146      return %ret;
147      }
148
149  # resultat de la commande arp
150   # tech7meylan.hmg.inpg.fr (194.254.66.240) at 00:14:22:45:28:A9 [ether] on eth0
151   my $cmd_arp  = `arp -a $param_ip_or_host 2>/dev/null`;
152   chomp $cmd_arp;
153   $cmd_arp =~ /(\S*)\s\(([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\)\sat\s([0-9,A-Z]{2}:[0-9,A-Z]{2}:[0-9,A-Z]{2}:[0-9,A-Z]{2}:[0-9,A-Z]{2}:[0-9,A-Z]{2})/;
154   $ret{hostname_fq}  = $1 if(defined($1));
155   $ret{ipv4_address} = $2 if(defined($2));
156   $ret{mac_address}  = $3 if(defined($3));
157
158#   if ($ret{ipv4_address} eq '0.0.0.0' and $ret{mac_address} eq 'unknow'and $ret{hostname_fq} eq 'unknow') {
159      # resultat de la commande host si le parametre est ip
160      # 250.66.254.194.in-addr.arpa domain name pointer legihp2100.hmg.inpg.fr.
161      my $cmd_host = `host $param_ip_or_host 2>/dev/null`;
162      chomp $cmd_host;
163      $cmd_host =~ m/domain\sname\spointer\s(\S+)\.$/;
164      $ret{hostname_fq} = $1 if defined $1;
165
166      # resultat de la commande host si parametre est hostname
167      # tech7meylan.hmg.inpg.fr has address 194.254.66.240
168      $cmd_host =~ m/\shas\saddress\s([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})$/;
169      $ret{ipv4_address} = $1 if defined $1;
170
171      $cmd_host =~ m/\b([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.in-addr\.arpa\s/;
172      $ret{ipv4_address} = "$4.$3.$2.$1"     if defined $1 and  defined $2 and  defined $3 and  defined $4;
173      $ret{hostname_fq}  = $param_ip_or_host if not defined $1 and $ret{hostname_fq} eq 'unknow';
174#      }
175
176   unless ($ret{mac_address} eq 'unknow') {
177      my @paquets = ();
178      foreach ( split(/:/, $ret{mac_address}) ) {
179         my @chars = split //, uc("00$_");
180         push @paquets, "$chars[-2]$chars[-1]";
181         }
182      $ret{mac_address} = join ':', @paquets;
183      }
184
185   return %ret;
186   }
187
188###
189# va rechercher le nom des switchs pour savoir qui est qui
190sub init_switch_names {
191   my $verbose = shift;
192   
193   printf "%-25s                %-25s %s\n",'Switch','Description','Type';
194#   print "Switch description\n" if $verbose;
195   print "-------------------------------------------------------------------------\n" if $verbose;
196
197   INIT_EACH_SWITCH:
198   for my $sw (@SWITCH) {
199      my %session = ( -hostname   => $sw->{hostname} );
200         $session{-version} = $sw->{version}   || 1;
201         $session{-port}    = $sw->{snmpport}  || $DEFAULT{snmpport}  || 161;
202         if (exists $sw->{version} and $sw->{version} eq 3) {
203            $session{-username} = $sw->{username} || 'snmpadmin';
204            }
205         else {
206            $session{-community} = $sw->{community} || $DEFAULT{community} || 'public';
207            }
208
209      $sw->{local_session} = \%session;
210
211      my ($session, $error) = Net::SNMP->session( %{$sw->{local_session}} );
212      print "$error \n" if $error;
213
214      my $result = $session->get_request(
215         -varbindlist => [
216            $OID_NUMBER{sysDescr},
217            $OID_NUMBER{sysName},
218            $OID_NUMBER{sysContact},
219            $OID_NUMBER{sysLocation},
220            ]
221         );
222      $sw->{description} = $result->{$OID_NUMBER{sysName}} || $sw->{hostname};
223      #$sw->{location} = $result->{"1.3.6.1.2.1.1.6.0"} || $sw->{hostname};
224      #$sw->{contact} = $result->{"1.3.6.1.2.1.1.4.0"} || $sw->{hostname};
225      $session->close;
226 
227      my ($desc, $type) = split ':', $sw->{description}, 2;
228      printf "%-25s 0--------->>>> %-25s %s\n", $sw->{hostname}, $desc, uc($type) if $verbose;
229      }
230
231   print "\n" if $verbose;
232   }
233
234###
235# convertit l'hexa (uniquement 2 chiffres) en decimal
236sub hex_to_dec {
237   #00:0F:1F:43:E4:2B
238   my $car = '00' . uc(shift);
239
240   return '00' if $car eq '00UNKNOW';
241   my %table = (
242      "0"=>"0",  "1"=>"1",  "2"=>"2",  "3"=>"3",  "4"=>"4",  "5"=>"5", "6"=>"6", "7"=>"7", "8"=>"8", "9"=>"9",
243      "A"=>"10", "B"=>"11", "C"=>"12", "D"=>"13", "E"=>"14", "F"=>"15"
244      );
245   my @chars = split(//, $car);
246   return $table{$chars[-2]}*16 + $table{$chars[-1]};
247   }
248
249###
250# convertit l'@ arp en decimal
251sub arp_hex_to_dec {
252   #00:0F:1F:43:E4:2B
253   my $arp = shift;
254
255   my @paquets = split /:/, $arp;
256   my $return = '';
257   foreach(@paquets) {
258      $return .= ".".hex_to_dec($_);
259      }
260   return $return;
261   }
262
263###
264# va rechercher le port et le switch sur lequel est la machine
265sub find_switch_port {
266   my $arp = shift;
267   my $switch_proposal = shift || '';
268   
269   my %ret;
270   $ret{switch_description} = "unknow";
271   $ret{switch_port} = "0";
272
273   return %ret if $arp eq 'unknow';;
274
275   my @SWITCH_search = @SWITCH;
276   if ($switch_proposal ne '') {
277      for my $sw (@SWITCH) {
278         next if $sw->{hostname} ne $switch_proposal;
279         unshift @SWITCH_search, $sw;
280         last;
281         }
282      }
283
284   my $research = "1.3.6.1.2.1.17.4.3.1.2".arp_hex_to_dec($arp);
285   
286   LOOP_ON_SWITCH:
287   for my $sw (@SWITCH_search) {
288      my ($session, $error) = Net::SNMP->session( %{$sw->{local_session}} );
289print "$error \n" if $error;
290#         -hostname   => $sw->{hostname},
291#         -community  => $sw->{community} || $DEFAULT{community} || 'public',
292#         -port       => $sw->{snmpport}  || $DEFAULT{snmpport}  || 161
293#         );
294#print "$sw->{hostname} --  $research \n";
295      my $result = $session->get_request(
296         -varbindlist => [$research]
297         );
298#      if(defined($result)) {
299      if (not defined($result) or $result->{$research} eq 'noSuchInstance') {
300#print "$sw->{hostname} --  $research --".$session->error()."\n";
301         $session->close;
302         next LOOP_ON_SWITCH;
303         }
304
305         my $swport = $result->{$research};
306         $session->close;
307
308         # IMPORTANT !!
309         # ceci empeche la detection sur certains port ...
310         # en effet les switch sont relies entre eux par un cable reseau et du coup
311         # tous les arp de toutes les machines sont presentes sur ces ports (ceux choisis ici sont les miens)
312         # cette partie est a ameliore, voir a configurer dans l'entete
313         # 21->24 45->48
314#         my $flag = 0;
315         SWITCH_PORT_IGNORE:
316         foreach my $p (@{$sw->{portignore}}) {
317            next SWITCH_PORT_IGNORE if $swport ne get_numerical_port($sw->{hostname},$p);
318#            $flag = 1;
319            next LOOP_ON_SWITCH;
320            }
321#         if ($flag == 0) {
322            $ret{switch_hostname}    = $sw->{hostname};
323            $ret{switch_description} = $sw->{description};
324            $ret{switch_port}        = get_human_readable_port($sw->{hostname}, $swport); # $swport;
325           
326            last LOOP_ON_SWITCH;
327#            }
328#         }
329#      $session->close;
330      }
331   return %ret;
332   }
333
334###
335# va rechercher les port et les switch sur lequel est la machine
336sub find_all_switch_port {
337   my $arp = shift;
338
339   my $ret = {};
340
341   return $ret if $arp eq 'unknow';
342
343   for my $sw (@SWITCH) {
344      $SWITCH_PORT_COUNT{$sw->{hostname}} = {} if not exists $SWITCH_PORT_COUNT{$sw->{hostname}};
345      }
346
347   my $research = "1.3.6.1.2.1.17.4.3.1.2".arp_hex_to_dec($arp);
348   LOOP_ON_ALL_SWITCH:
349   for my $sw (@SWITCH) {
350      my ($session, $error) = Net::SNMP->session( %{$sw->{local_session}} );
351      print "$error \n" if $error;
352
353      my $result = $session->get_request(
354         -varbindlist => [$research]
355         );
356
357      if(defined($result) and $result->{$research} ne 'noSuchInstance'){
358         my $swport = $result->{$research};
359
360         $ret->{$sw->{hostname}} = {};
361         $ret->{$sw->{hostname}}{hostname}    = $sw->{hostname};
362         $ret->{$sw->{hostname}}{description} = $sw->{description};
363         $ret->{$sw->{hostname}}{port}        = get_human_readable_port($sw->{hostname}, $swport);
364
365         $SWITCH_PORT_COUNT{$sw->{hostname}}->{$swport}++;
366         }
367
368      $session->close;
369      }
370   return $ret;
371   }
372
373sub get_list_network {
374
375   return keys %{$KLASK_CFG->{network}};
376   }
377
378sub get_current_interface {
379   my $network = shift;
380
381   return $KLASK_CFG->{network}{$network}{interface};
382   }
383 
384###
385# liste l'ensemble des adresses ip d'un réseau
386sub get_list_ip {
387   my @network = @_;
388
389   my $cidrlist = Net::CIDR::Lite->new;
390
391   for my $net (@network) {
392      my @line  = @{$KLASK_CFG->{network}{$net}{'ip-subnet'}};
393      for my $cmd (@line) {
394         for my $method (keys %$cmd){
395            $cidrlist->add_any($cmd->{$method}) if $method eq 'add';
396            }
397         }
398      }
399
400   my @res = ();
401
402   for my $cidr ($cidrlist->list()) {
403      my $net = new NetAddr::IP $cidr;
404      for my $ip (@$net) {
405         $ip =~ s#/32##;
406         push @res,  $ip;
407         }
408      }
409
410   return @res;
411   }
412
413# liste l'ensemble des routeurs du réseau
414sub get_list_main_router {
415   my @network = @_;
416
417   my @res = ();
418
419   for my $net (@network) {
420      push @res, $KLASK_CFG->{network}{$net}{'main-router'};
421      }
422
423   return @res;
424   }
425
426sub get_human_readable_port {
427   my $sw = shift;
428   my $port = shift;
429   
430   return $port if not $sw eq 'sw8000-batA.hmg.priv';
431   
432   my $reste = (($port - 1) % 8) + 1;
433   my $major = int( ($port - 1) / 8 );
434
435   return "$INTERNAL_PORT_MAP{$major}$reste";
436   }
437
438sub get_numerical_port {
439   my $sw   = shift;
440   my $port = shift;
441   
442   return $port if not $sw eq 'sw8000-batA.hmg.priv';
443
444   my $letter = substr($port, 0, 1);
445   
446#   return $port if $letter =~ m/\d/;
447   
448   my $reste =  substr($port, 1);
449   
450   return $INTERNAL_PORT_MAP_REV{$letter} * 8 + $reste;
451   }
452
453################
454# Les commandes
455################
456
457sub cmd_help {
458
459print <<END;
460klask - ports manager and finder for switch
461
462 klask updatedb
463 klask exportdf
464
465 klask searchdb computer
466 klask search   computer
467
468 klask enable  switch port
469 klask disable switch port
470 klask status  switch port
471END
472   }
473
474sub cmd_search {
475   my @computer = @_;
476   
477   init_switch_names();    #nomme les switchs
478   fastping(@computer);
479   for my $clientname (@computer) {
480      my %resol_arp = resolve_ip_arp_host($clientname);          #resolution arp
481      my %where     = find_switch_port($resol_arp{mac_address}); #retrouve l'emplacement
482      printf "%-22s %2i %-30s %-15s %18s", $where{switch_description}, $where{switch_port}, $resol_arp{hostname_fq}, $resol_arp{ipv4_address}, $resol_arp{mac_address}."\n"
483         unless $where{switch_description} eq 'unknow' and $resol_arp{hostname_fq} eq 'unknow' and $resol_arp{mac_address} eq 'unknow';
484      }
485   }
486
487sub cmd_searchdb {
488   my @computer = @_;
489
490   fastping(@computer);
491   my $computerdb = YAML::LoadFile("$KLASK_DB_FILE");
492   
493   LOOP_ON_COMPUTER:
494   for my $clientname (@computer) {
495      my %resol_arp = resolve_ip_arp_host($clientname);      #resolution arp
496      my $ip = $resol_arp{ipv4_address};
497     
498      next LOOP_ON_COMPUTER unless exists $computerdb->{$ip};
499     
500      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($computerdb->{$ip}{timestamp});
501      $year += 1900;
502      $mon++;
503      my $date = sprintf "%04i-%02i-%02i %02i:%02i", $year,$mon,$mday,$hour,$min;
504
505      printf "%-22s %2s %-30s %-15s %-18s %s\n",
506         $computerdb->{$ip}{switch_name},
507         $computerdb->{$ip}{switch_port},
508         $computerdb->{$ip}{hostname_fq},
509         $ip,
510         $computerdb->{$ip}{mac_address},
511         $date;
512      }
513   }
514
515sub cmd_updatedb {
516   my @network = @_;
517      @network = get_list_network() if not @network;
518
519   my $computerdb = YAML::LoadFile("$KLASK_DB_FILE");
520   my $timestamp = time;
521   
522   my %computer_not_detected = ();
523   my $timestamp_last_week = $timestamp - (3600 * 24 * 7);
524
525   my $number_of_computer = get_list_ip(@network); # + 1;
526   my $size_of_database   = keys %$computerdb;
527   my $i = 0;
528   my $detected_computer = 0;
529   
530   init_switch_names('yes');    #nomme les switchs
531
532   my %router_mac_ip = ();
533   DETECT_ALL_ROUTER:
534#   for my $one_router ('194.254.66.254') {
535   for my $one_router ( get_list_main_router(@network) ) {
536      my %resol_arp = resolve_ip_arp_host($one_router);
537      $router_mac_ip{ $resol_arp{mac_address} } = $resol_arp{ipv4_address};
538      }
539
540   ALL_NETWORK:
541   for my $net (@network) {
542
543      my @computer = get_list_ip($net);
544      my $current_interface = get_current_interface($net);
545
546      fastping(@computer);
547
548      LOOP_ON_COMPUTER:
549      for my $one_computer (@computer) {
550         $i++;
551         
552         my $total_percent = int(($i*100)/$number_of_computer);
553
554         my $localtime = time - $timestamp;
555         my ($sec,$min) = localtime($localtime);
556
557         my $time_elapse = 0;
558            $time_elapse = $localtime * ( 100 - $total_percent) / $total_percent if $total_percent != 0;
559         my ($sec_elapse,$min_elapse) = localtime($time_elapse);
560
561         printf "\rComputer scanned: %4i/%i (%2i%%)",  $i,                 $number_of_computer, $total_percent;
562#         printf ", Computer detected: %4i/%i (%2i%%)", $detected_computer, $size_of_database,   int(($detected_computer*100)/$size_of_database);
563         printf ", detected: %4i/%i (%2i%%)", $detected_computer, $size_of_database,   int(($detected_computer*100)/$size_of_database);
564         printf " [Time: %02i:%02i / %02i:%02i]", int($localtime/60), $localtime % 60, int($time_elapse/60), $time_elapse % 60;
565#         printf "  [%02i:%02i/%02i:%02i]", int($localtime/60), $localtime % 60, int($time_elapse/60), $time_elapse % 60;
566         printf " %-14s", $one_computer;
567
568         my %resol_arp = resolve_ip_arp_host($one_computer,$current_interface);
569         
570         # do not search on router connection (why ?)
571         if ( exists $router_mac_ip{$resol_arp{mac_address}}) {
572            $computer_not_detected{$one_computer} = $current_interface;
573            next LOOP_ON_COMPUTER;
574            }
575
576         # do not search on switch inter-connection
577         if (exists $switch_level{$resol_arp{hostname_fq}}) {
578            $computer_not_detected{$one_computer} = $current_interface;
579            next LOOP_ON_COMPUTER;
580            }
581
582         my $switch_proposal = '';
583         if (exists $computerdb->{$resol_arp{ipv4_address}} and exists $computerdb->{$resol_arp{ipv4_address}}{switch_hostname}) {
584            $switch_proposal = $computerdb->{$resol_arp{ipv4_address}}{switch_hostname};
585            }
586
587         # do not have a mac address
588         if ($resol_arp{mac_address} eq 'unknow' or (exists $resol_arp{timestamps} and $resol_arp{timestamps} < ($timestamp - 3 * 3600))) {
589            $computer_not_detected{$one_computer} = $current_interface;
590            next LOOP_ON_COMPUTER;
591            }
592
593         my %where = find_switch_port($resol_arp{mac_address},$switch_proposal);
594
595         #192.168.24.156:
596         #  arp: 00:0B:DB:D5:F6:65
597         #  hostname: pcroyon.hmg.priv
598         #  port: 5
599         #  switch: sw-batH-legi:hp2524
600         #  timestamp: 1164355525
601
602         # do not have a mac address
603#         if ($resol_arp{mac_address} eq 'unknow') {
604#            $computer_not_detected{$one_computer} = $current_interface;
605#            next LOOP_ON_COMPUTER;
606#            }
607
608         # detected on a switch
609         if ($where{switch_description} ne 'unknow') {
610            $detected_computer++;
611            $computerdb->{$resol_arp{ipv4_address}} = {
612               hostname_fq        => $resol_arp{hostname_fq},
613               mac_address        => $resol_arp{mac_address},
614               switch_hostname    => $where{switch_hostname},
615               switch_description => $where{switch_description},
616               switch_port        => $where{switch_port},
617               timestamp          => $timestamp,
618               };
619            next LOOP_ON_COMPUTER;
620            }
621
622         # new in the database but where it is ?
623         if (not exists $computerdb->{$resol_arp{ipv4_address}}) {
624            $detected_computer++;
625            $computerdb->{$resol_arp{ipv4_address}} = {
626               hostname_fq        => $resol_arp{hostname_fq},
627               mac_address        => $resol_arp{mac_address},
628               switch_hostname    => $where{switch_hostname},
629               switch_description => $where{switch_description},
630               switch_port        => $where{switch_port},
631               timestamp          => $resol_arp{timestamp},
632               };
633            }
634
635         # mise a jour du nom de la machine si modification dans le dns
636         $computerdb->{$resol_arp{ipv4_address}}{hostname_fq} = $resol_arp{hostname_fq};
637       
638         # mise à jour de la date de détection si détection plus récente par arpwatch
639         $computerdb->{$resol_arp{ipv4_address}}{timestamp}   = $resol_arp{timestamp} if exists $resol_arp{timestamp} and $computerdb->{$resol_arp{ipv4_address}}{timestamp} < $resol_arp{timestamp};
640
641         # provisoire car changement de nom des attributs
642#         $computerdb->{$resol_arp{ipv4_address}}{mac_address}        = $computerdb->{$resol_arp{ipv4_address}}{arp};
643#         $computerdb->{$resol_arp{ipv4_address}}{switch_description} = $computerdb->{$resol_arp{ipv4_address}}{switch};
644#         $computerdb->{$resol_arp{ipv4_address}}{switch_port}        = $computerdb->{$resol_arp{ipv4_address}}{port};
645       
646         # relance un arping sur la machine si celle-ci n'a pas été détectée depuis plus d'une semaine
647#         push @computer_not_detected, $resol_arp{ipv4_address} if $computerdb->{$resol_arp{ipv4_address}}{timestamp} < $timestamp_last_week;
648         $computer_not_detected{$resol_arp{ipv4_address}} = $current_interface if $computerdb->{$resol_arp{ipv4_address}}{timestamp} < $timestamp_last_week;
649       
650         }
651      }
652
653   # final end of line at the end of the loop
654   printf "\n";
655
656   my $dirdb = $KLASK_DB_FILE;
657      $dirdb =~ s#/[^/]*$##;
658   mkdir "$dirdb", 0755 unless -d "$dirdb";
659   YAML::DumpFile("$KLASK_DB_FILE", $computerdb);
660
661   for my $one_computer (keys %computer_not_detected) {
662      my $interface = $computer_not_detected{$one_computer};
663      system "arping -c 1 -w 1 -rR -i $interface $one_computer &>/dev/null";
664#      print  "arping -c 1 -w 1 -rR -i $interface $one_computer 2>/dev/null\n";
665      }
666   }
667
668sub cmd_removedb {
669   my @computer = @_;
670   
671   my $computerdb = YAML::LoadFile("$KLASK_DB_FILE");
672
673   LOOP_ON_COMPUTER:
674   for my $one_computer (@computer) {
675
676      my %resol_arp = resolve_ip_arp_host($one_computer);
677
678      delete $computerdb->{$resol_arp{ipv4_address}} if exists $computerdb->{$resol_arp{ipv4_address}};
679      }
680
681   my $dirdb = $KLASK_DB_FILE;
682      $dirdb =~ s#/[^/]*$##;
683   mkdir "$dirdb", 0755 unless -d "$dirdb";
684   YAML::DumpFile("$KLASK_DB_FILE", $computerdb);
685   }
686
687sub cmd_exportdb {
688   my $computerdb = YAML::LoadFile("$KLASK_DB_FILE");
689
690   printf "%-24s %-4s            %-30s %-15s %-18s %-s\n", qw(Switch Port Hostname IPv4-Address MAC-Address Date);
691   print "---------------------------------------------------------------------------------------------------------------------------\n";
692
693   LOOP_ON_IP_ADDRESS:
694   foreach my $ip (Net::Netmask::sort_by_ip_address(keys %$computerdb)) {
695   
696#      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq 'unknow';
697
698      # to be improve in the future
699      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !
700
701# dans le futur
702#      next if $computerdb->{$ip}{hostname_fq} eq 'unknow';
703     
704      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($computerdb->{$ip}{timestamp});
705      $year += 1900;
706      $mon++;
707      my $date = sprintf "%04i-%02i-%02i %02i:%02i", $year,$mon,$mday,$hour,$min;
708
709      printf "%-25s  %2s  <-------  %-30s %-15s %-18s %s\n",
710         $computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description},
711         $computerdb->{$ip}{switch_port},
712         $computerdb->{$ip}{hostname_fq},
713         $ip,
714         $computerdb->{$ip}{mac_address},
715         $date;
716      }
717   }
718
719sub cmd_iplocation {
720   my $computerdb = YAML::LoadFile("$KLASK_DB_FILE");
721
722   LOOP_ON_IP_ADDRESS:
723   foreach my $ip (Net::Netmask::sort_by_ip_address(keys %$computerdb)) {
724
725      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !
726
727      my $sw_hostname = $computerdb->{$ip}{switch_hostname} || '';
728      next if $sw_hostname eq 'unknow';
729 
730      my $sw_location = '';
731      for my $sw (@SWITCH) {
732         next if $sw_hostname ne $sw->{hostname};
733         $sw_location = $sw->{location};
734         last;
735         }
736
737      printf "%s: \"%s\"\n", $ip, $sw_location if not $sw_location eq '';
738      }
739   }
740
741sub cmd_enable {
742   my $switch = shift;
743   my $port   = shift;
744   
745   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 1 (up)
746   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 2 (down)
747   system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 1";
748   }
749
750sub cmd_disable {
751   my $switch = shift;
752   my $port   = shift;
753   
754   system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 2";
755   }
756
757sub cmd_status {
758   my $switch = shift;
759   my $port   = shift;
760   
761   system "snmpget -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port";
762   }
763
764
765sub cmd_updatesw {
766
767   init_switch_names('yes');    #nomme les switchs
768   print "\n";
769
770   my %where = ();
771   my %db_switch_output_port = ();
772   my %db_switch_ip_hostname = ();
773
774   DETECT_ALL_ROUTER:
775#   for my $one_computer ('194.254.66.254') {
776   for my $one_router ( get_list_main_router(get_list_network()) ) {
777      my %resol_arp = resolve_ip_arp_host($one_router,'*','low');            # resolution arp
778      next DETECT_ALL_ROUTER if $resol_arp{mac_address} eq 'unknow';
779     
780      $where{$resol_arp{ipv4_address}} = find_all_switch_port($resol_arp{mac_address}); # retrouve les emplacements des routeurs
781      }
782
783   ALL_ROUTER_IP_ADDRESS:
784   for my $ip (Net::Netmask::sort_by_ip_address(keys %where)) { # '194.254.66.254')) {
785   
786      next ALL_ROUTER_IP_ADDRESS if not exists $where{$ip}; # /a priori/ idiot car ne sers à rien...
787
788      ALL_SWITCH_CONNECTED:
789      for my $switch_detected ( keys %{$where{$ip}} ) {
790
791         my $switch = $where{$ip}->{$switch_detected};
792
793         next ALL_SWITCH_CONNECTED if $switch->{port} eq '0';
794         
795         $db_switch_output_port{$switch->{hostname}} = $switch->{port};
796         }
797      }   
798
799#   print "Switch output port\n"; 
800#   print "------------------\n";
801#   for my $sw (sort keys %db_switch_output_port) {
802#      printf "%-25s %2s\n", $sw, $db_switch_output_port{$sw};
803#      }
804#   print "\n";
805
806
807   my %db_switch_link_with = ();
808
809   my @list_switch_ip = ();
810   my @list_switch_ipv4 = ();
811   for my $sw (@SWITCH){
812      push @list_switch_ip, $sw->{hostname};
813      }
814
815   ALL_SWITCH:
816   for my $one_computer (@list_switch_ip) {
817      my %resol_arp = resolve_ip_arp_host($one_computer,'*','low'); # arp resolution
818      next ALL_SWITCH if $resol_arp{mac_address} eq 'unknow';
819     
820      push @list_switch_ipv4,$resol_arp{ipv4_address};
821     
822      $where{$resol_arp{ipv4_address}} = find_all_switch_port($resol_arp{mac_address}); # find port on all switch
823
824      $db_switch_ip_hostname{$resol_arp{ipv4_address}} = $resol_arp{hostname_fq};
825      }
826     
827   ALL_SWITCH_IP_ADDRESS:
828   for my $ip (Net::Netmask::sort_by_ip_address(@list_switch_ipv4)) {
829   
830      next ALL_SWITCH_IP_ADDRESS if not exists $where{$ip};
831
832      DETECTED_SWITCH:
833      for my $switch_detected ( keys %{$where{$ip}} ) {
834
835         next DETECTED_SWITCH if not exists $SWITCH_PORT_COUNT{ $db_switch_ip_hostname{$ip}};
836
837         my $switch = $where{$ip}->{$switch_detected};
838
839         next if $switch->{port}     eq '0';
840         next if $switch->{port}     eq $db_switch_output_port{$switch->{hostname}};
841         next if $switch->{hostname} eq $db_switch_ip_hostname{$ip}; # $computerdb->{$ip}{hostname};
842
843         $db_switch_link_with{ $db_switch_ip_hostname{$ip} } ||= {};
844         $db_switch_link_with{ $db_switch_ip_hostname{$ip} }->{ $switch->{hostname} } = $switch->{port};
845         }
846
847      }
848   
849   my %db_switch_connected_on_port = ();
850   my $maybe_more_than_one_switch_connected = 'yes';
851   
852   while ($maybe_more_than_one_switch_connected eq 'yes') {
853      for my $sw (keys %db_switch_link_with) {
854         for my $connect (keys %{$db_switch_link_with{$sw}}) {
855         
856            my $port = $db_switch_link_with{$sw}->{$connect};
857         
858            $db_switch_connected_on_port{"$connect:$port"} ||= {};
859            $db_switch_connected_on_port{"$connect:$port"}->{$sw}++; # Just to define the key
860            }
861         }
862
863      $maybe_more_than_one_switch_connected  = 'no';
864
865      SWITCH_AND_PORT:
866      for my $swport (keys %db_switch_connected_on_port) {
867         
868         next if keys %{$db_switch_connected_on_port{$swport}} == 1;
869         
870         $maybe_more_than_one_switch_connected = 'yes';
871
872         my ($sw_connect,$port_connect) = split ':', $swport;
873         my @sw_on_same_port = keys %{$db_switch_connected_on_port{$swport}};
874
875         CONNECTED:
876         for my $sw_connected (@sw_on_same_port) {
877           
878            next CONNECTED if not keys %{$db_switch_link_with{$sw_connected}} == 1;
879           
880            $db_switch_connected_on_port{$swport} = {$sw_connected => 1};
881           
882            for my $other_sw (@sw_on_same_port) {
883               next if $other_sw eq $sw_connected;
884               
885               delete $db_switch_link_with{$other_sw}->{$sw_connect};
886               }
887           
888            # We can not do better for this switch for this loop
889            next SWITCH_AND_PORT;
890            }
891         }
892      }
893
894   my %db_switch_parent =();
895
896   for my $sw (keys %db_switch_link_with) {
897      for my $connect (keys %{$db_switch_link_with{$sw}}) {
898     
899         my $port = $db_switch_link_with{$sw}->{$connect};
900     
901         $db_switch_connected_on_port{"$connect:$port"} ||= {};
902         $db_switch_connected_on_port{"$connect:$port"}->{$sw} = $port;
903       
904         $db_switch_parent{$sw} = {switch => $connect, port => $port};
905         }
906      }
907
908   print "Switch output port and parent port connection\n"; 
909   print "---------------------------------------------\n";
910   for my $sw (sort keys %db_switch_output_port) {
911      if (exists $db_switch_parent{$sw}) {
912         printf "%-25s  %2s  +-->  %2s  %-25s\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{port}, $db_switch_parent{$sw}->{switch};
913         }
914      else {
915         printf "%-25s  %2s  +-->  router\n", $sw, $db_switch_output_port{$sw};
916         }
917      }
918   print "\n";
919
920   print "Switch parent and children port inter-connection\n";
921   print "------------------------------------------------\n";
922   for my $swport (sort keys %db_switch_connected_on_port) {       
923      my ($sw_connect,$port_connect) = split ':', $swport;
924      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
925         if (exists $db_switch_output_port{$sw}) {
926            printf "%-25s  %2s  <--+  %2s  %-25s\n", $sw_connect, $port_connect, $db_switch_output_port{$sw}, $sw;
927            }
928         else {
929            printf "%-25s  %2s  <--+      %-25s\n", $sw_connect, $port_connect, $sw;
930            }
931         }
932      }
933
934   my $switch_connection = {
935      output_port       => \%db_switch_output_port,
936      parent            => \%db_switch_parent,
937      connected_on_port => \%db_switch_connected_on_port,
938      link_with         => \%db_switch_link_with,
939      };
940     
941   YAML::DumpFile("$KLASK_SW_FILE", $switch_connection);
942   }
943
944sub cmd_exportsw {
945   my @ARGV   = @_;
946
947   my $format = 'txt';
948
949   my $ret = GetOptions(
950      'format|f=s'  => \$format,
951      );
952
953   my %possible_format = (
954      txt => \&cmd_exportsw_txt,
955      dot => \&cmd_exportsw_dot,
956      );
957
958   $format = 'txt' if not defined $possible_format{$format};
959   
960   $possible_format{$format}->(@ARGV);
961   }
962
963sub cmd_exportsw_txt {
964
965   my $switch_connection = YAML::LoadFile("$KLASK_SW_FILE");
966
967   my %db_switch_output_port       = %{$switch_connection->{output_port}};
968   my %db_switch_parent            = %{$switch_connection->{parent}};
969   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};
970
971   print "Switch output port and parent port connection\n"; 
972   print "---------------------------------------------\n";
973   for my $sw (sort keys %db_switch_output_port) {
974      if (exists $db_switch_parent{$sw}) {
975         printf "%-25s  %2s  +-->  %2s  %-25s\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{port}, $db_switch_parent{$sw}->{switch};
976         }
977      else {
978         printf "%-25s  %2s  +-->  router\n", $sw, $db_switch_output_port{$sw};
979         }
980      }
981   print "\n";
982
983   print "Switch parent and children port inter-connection\n";
984   print "------------------------------------------------\n";
985   for my $swport (sort keys %db_switch_connected_on_port) {       
986      my ($sw_connect,$port_connect) = split ':', $swport;
987      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
988         if (exists $db_switch_output_port{$sw}) {
989            printf "%-25s  %2s  <--+  %2s  %-25s\n", $sw_connect, $port_connect, $db_switch_output_port{$sw}, $sw;
990            }
991         else {
992            printf "%-25s  %2s  <--+      %-25s\n", $sw_connect, $port_connect, $sw;
993            }
994         }
995      }
996   }
997
998sub cmd_exportsw_dot {
999
1000   my $switch_connection = YAML::LoadFile("$KLASK_SW_FILE");
1001   
1002   my %db_switch_output_port       = %{$switch_connection->{output_port}};
1003   my %db_switch_parent            = %{$switch_connection->{parent}};
1004   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};
1005   my %db_switch_link_with         = %{$switch_connection->{link_with}};
1006     
1007   my %db_building= ();
1008   for my $sw (@SWITCH) {
1009      my ($building, $location) = split /\//, $sw->{location}, 2;
1010      $db_building{$building} ||= {};
1011      $db_building{$building}->{$location} ||= {};
1012      $db_building{$building}->{$location}{ $sw->{hostname} } = 'y';
1013      }
1014 
1015 
1016   print "digraph G {\n";
1017
1018   print "site [label = \"site\", color = black, fillcolor = gold, shape = invhouse, style = filled];\n";
1019   print "internet [label = \"internet\", color = black, fillcolor = cyan, shape = house, style = filled];\n";
1020
1021   my $b=0;
1022   for my $building (keys %db_building) {
1023      $b++;
1024     
1025      print "\"building$b\" [label = \"$building\", color = black, fillcolor = gold, style = filled];\n";
1026      print "site -> \"building$b\" [len = 2, color = firebrick];\n";
1027
1028      my $l = 0;
1029      for my $loc (keys %{$db_building{$building}}) {
1030         $l++;
1031 
1032         print "\"location$b-$l\" [label = \"$building / $loc\", color = black, fillcolor = orange, style = filled];\n";
1033         print "\"building$b\" -> \"location$b-$l\" [len = 2, color = firebrick]\n";
1034
1035         for my $sw (keys %{$db_building{$building}->{$loc}}) {
1036
1037            print "\"$sw:$db_switch_output_port{$sw}\" [label = $db_switch_output_port{$sw}, color = black, fillcolor = lightblue,  peripheries = 2, style = filled];\n";
1038
1039            print "\"$sw\" [label = \"$sw\", color = black, fillcolor = palegreen, shape = rect, style = filled];\n";
1040            print "\"location$b-$l\" -> \"$sw\" [len = 2, color = firebrick, arrowtail = dot]\n";
1041            print "\"$sw\" -> \"$sw:$db_switch_output_port{$sw}\" [len=2, style=bold, arrowhead = normal, arrowtail = invdot]\n";
1042
1043
1044            for my $swport (keys %db_switch_connected_on_port) {
1045               my ($sw_connect,$port_connect) = split ':', $swport;
1046               next if not $sw_connect eq $sw;
1047               next if $port_connect eq $db_switch_output_port{$sw};
1048               print "\"$sw:$port_connect\" [label = $port_connect, color = black, fillcolor = plum,  peripheries = 1, style = filled];\n";
1049               print "\"$sw:$port_connect\" -> \"$sw\" [len=2, style=bold, arrowhead= normal, arrowtail = inv]\n";
1050              }
1051            }
1052         }
1053      }
1054
1055#   print "Switch output port and parent port connection\n"; 
1056#   print "---------------------------------------------\n";
1057   for my $sw (sort keys %db_switch_output_port) {
1058      if (exists $db_switch_parent{$sw}) {
1059#         printf "   \"%s:%s\" -> \"%s:%s\"\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{switch}, $db_switch_parent{$sw}->{port};
1060         }
1061      else {
1062         printf "   \"%s:%s\" -> internet\n", $sw, $db_switch_output_port{$sw};
1063         }
1064      }
1065   print "\n";
1066
1067#   print "Switch parent and children port inter-connection\n";
1068#   print "------------------------------------------------\n";
1069   for my $swport (sort keys %db_switch_connected_on_port) {       
1070      my ($sw_connect,$port_connect) = split ':', $swport;
1071      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
1072         if (exists $db_switch_output_port{$sw}) {
1073            printf "   \"%s:%s\" -> \"%s:%s\" [color = navyblue]\n", $sw, $db_switch_output_port{$sw}, $sw_connect, $port_connect;
1074            }
1075         else {
1076            printf "   \"%s\"   -> \"%s%s\"\n", $sw, $sw_connect, $port_connect;
1077            }
1078         }
1079      }
1080
1081print "}\n";
1082   }
1083
1084
1085__END__
1086
1087
1088=head1 NAME
1089
1090klask - ports manager and finder for switch
1091
1092
1093=head1 SYNOPSIS
1094
1095 klask updatedb
1096 klask exportdb
1097
1098 klask updatesw
1099 klask exportsw --format [txt|dot]
1100
1101 klask searchdb computer
1102 klask search   computer
1103
1104 klask enable  switch port
1105 klask disable swith port
1106 klask status  swith port
1107
1108
1109=head1 DESCRIPTION
1110
1111klask is a small tool to find where is a host in a big network. klask mean search in brittany.
1112
1113Klask has now a web site dedicated for it !
1114
1115 http://servforge.legi.inpg.fr/projects/klask
1116
1117
1118=head1 COMMANDS
1119
1120
1121=head2 search
1122
1123This command takes one or more computer in argument. It search a computer on the network and give the port and the switch on which the computer is connected.
1124
1125
1126=head2 enable
1127
1128This command activate a port on a switch by snmp. So you need to give the switch and the port number on the command line.
1129
1130
1131=head2 disable
1132
1133This command deactivate a port on a switch by snmp. So you need to give the switch and the port number on the command line.
1134
1135
1136=head2 status
1137
1138This command return the status of a port number on a switch by snmp. So you need to give the switch name and the port number on the command line.
1139
1140
1141=head2 updatedb
1142
1143This command will scan networks and update a database. To know which are the cmputer scan, you have to configure the file /etc/klask.conf This file is easy to read and write because klask use YAML format and not XML.
1144
1145
1146=head2 exportdb
1147
1148This command print the content of the database. There is actually only one format. It's very easy to have more format, it's just need times...
1149
1150
1151=head2 updatesw
1152
1153This command build a map of your manageable switch on your network. The list of the switch must be given in the file /etc/klask.conf.
1154
1155
1156=head2 exportsw --format [txt|dot]
1157
1158This command print the content of the switch database. There is actually two format. One is just txt for terminal and the other is the dot format from the graphviz environnement.
1159
1160 klask exportsw --format dot > /tmp/map.dot
1161 dot -Tpng /tmp/map.dot > /tmp/map.png
1162
1163
1164
1165=head1 CONFIGURATION
1166
1167Because klask need many parameters, it's not possible actually to use command line parameters. The configuration is done in a /etc/klask.conf YAML file. This format have many advantage over XML, it's easier to read and to write !
1168
1169Here an example, be aware with indent, it's important in YAML, do not use tabulation !
1170
1171 default:
1172   community: public
1173   snmpport: 161
1174
1175 network:
1176   labnet:
1177     ip-subnet:
1178       - add: 192.168.1.0/24
1179       - add: 192.168.2.0/24
1180     interface: eth0
1181     main-router: gw1.labnet.local
1182
1183   schoolnet:
1184     ip-subnet:
1185       - add: 192.168.6.0/24
1186       - add: 192.168.7.0/24
1187     interface: eth0.38
1188     main-router: gw2.schoolnet.local
1189
1190 switch:
1191   - hostname: sw1.klask.local
1192     portignore:
1193       - 1
1194       - 2
1195
1196   - hostname: sw2.klask.local
1197     location: BatK / 2 / K203
1198     type: HP2424
1199     portignore:
1200       - 1
1201       - 2
1202
1203I think it's pretty easy to understand. The default section can be overide in any section, if parameter mean something in theses sections. Network to be scan are define in the network section. You must put a add by network. Maybe i will make a delete line to suppress specific computers. The switch section define your switch. You have to write the port number to ignore, this is important if your switchs are cascade. Juste put the ports numbers between switch.
1204
1205
1206=head1 FILES
1207
1208 /etc/klask.conf
1209 /var/cache/klask/klaskdb
1210 /var/cache/klask/switchdb
1211
1212=head1 SEE ALSO
1213
1214Net::SNMP, Net::Netmask, Net::CIDR::Lite, NetAddr::IP, YAML
1215
1216
1217=head1 VERSION
1218
12190.4
1220
1221
1222=head1 AUTHOR
1223
1224Written by Gabriel Moreau, Grenoble - France
1225
1226
1227=head1 COPYRIGHT
1228       
1229Copyright (C) 2005-2008 Gabriel Moreau.
1230
1231
1232=head1 LICENCE
1233
1234GPL version 2 or later and Perl equivalent
Note: See TracBrowser for help on using the repository browser.