source: trunk/klask @ 215

Last change on this file since 215 was 215, checked in by g7moreau, 7 years ago
  • Small bug in test (forget not)
  • Property svn:executable set to *
  • Property svn:keywords set to Date Author Id Rev
File size: 95.6 KB
Line 
1#!/usr/bin/perl -w
2#
3# Copyright (C) 2005-2017 Gabriel Moreau
4#
5# $Id: klask 215 2017-02-18 13:25:55Z g7moreau $
6
7use strict;
8use warnings;
9use version; our $VERSION = qv('0.6.0');
10
11use Readonly;
12use FileHandle;
13use Net::SNMP;
14#use YAML;
15use YAML::Syck;
16use Net::Netmask;
17use Net::CIDR::Lite;
18use NetAddr::IP;
19use Getopt::Long qw(GetOptions);
20use Socket;
21use List::Util 'shuffle';
22
23# apt-get install snmp fping libnet-cidr-lite-perl libnet-netmask-perl libnet-snmp-perl libnetaddr-ip-perl libyaml-perl
24# libcrypt-des-perl libcrypt-hcesha-perl libdigest-hmac-perl
25# arping net-tools fping bind9-host arpwatch
26
27my $KLASK_VAR      = '/var/lib/klask';
28my $KLASK_CFG_FILE = '/etc/klask/klask.conf';
29my $KLASK_DB_FILE  = "$KLASK_VAR/klaskdb";
30my $KLASK_SW_FILE  = "$KLASK_VAR/switchdb";
31
32test_running_environnement();
33
34my $KLASK_CFG = YAML::Syck::LoadFile("$KLASK_CFG_FILE");
35
36my %DEFAULT     = %{$KLASK_CFG->{default}};
37my @SWITCH_LIST = @{$KLASK_CFG->{switch}};
38
39my %SWITCH_LEVEL = ();
40my %SWITCH_DB    = ();
41LEVEL_OF_EACH_SWITCH:
42for my $sw (@SWITCH_LIST) {
43   $SWITCH_LEVEL{$sw->{hostname}} = $sw->{level} || $DEFAULT{switch_level}  || 2;
44   $SWITCH_DB{$sw->{hostname}} = $sw;
45
46   # SNMP parameter initialisation
47   my %session = ( -hostname   => $sw->{hostname} );
48   $session{-version} = $sw->{version}   || 1;
49   $session{-port}    = $sw->{snmpport}  || $DEFAULT{snmpport}  || 161;
50   if (exists $sw->{version} and $sw->{version} eq '3') {
51      $session{-username} = $sw->{username} || 'snmpadmin';
52      }
53   else {
54      $session{-community} = $sw->{community} || $DEFAULT{community} || 'public';
55      }
56   $sw->{snmp_param_session} = \%session;
57   }
58@SWITCH_LIST = reverse sort { $SWITCH_LEVEL{$a->{hostname}} <=> $SWITCH_LEVEL{$b->{hostname}} } @{$KLASK_CFG->{switch}};
59
60#my %SWITCH_PORT_COUNT = ();
61
62my %CMD_DB = (
63   'help'                 => \&cmd_help,
64   'version'              => \&cmd_version,
65   'exportdb'             => \&cmd_exportdb,
66   'updatedb'             => \&cmd_updatedb,
67   'searchdb'             => \&cmd_searchdb,
68   'removedb'             => \&cmd_removedb,
69   'cleandb'              => \&cmd_cleandb,
70   'search'               => \&cmd_search,
71   'enable'               => \&cmd_enable,
72   'disable'              => \&cmd_disable,
73   'status'               => \&cmd_status,
74   'updatesw'             => \&cmd_updatesw,
75   'exportsw'             => \&cmd_exportsw,
76   'iplocation'           => \&cmd_ip_location,
77   'ip-free'              => \&cmd_ip_free,
78   'search-mac-on-switch' => \&cmd_search_mac_on_switch,
79   'bad-vlan-id'          => \&cmd_bad_vlan_id,
80   'set-vlan-port'        => \&cmd_set_vlan_port,
81   'get-vlan-port'        => \&cmd_get_vlan_port,
82   'set-vlan-name'        => \&cmd_set_vlan_name,
83   'get-vlan-name'        => \&cmd_get_vlan_name,
84   'rebootsw'             => \&cmd_rebootsw,
85   );
86
87Readonly my %INTERNAL_PORT_MAP => (
88   0 => 'A',
89   1 => 'B',
90   2 => 'C',
91   3 => 'D',
92   4 => 'E',
93   5 => 'F',
94   6 => 'G',
95   7 => 'H',
96   );
97Readonly my %INTERNAL_PORT_MAP_REV => reverse %INTERNAL_PORT_MAP;
98
99Readonly my %SWITCH_KIND => (
100   # HP
101   J3299A           => { type => 1, model => 'HP224M',         match => 'HP J3299A ProCurve Switch 224M'       },
102   J4120A           => { type => 1, model => 'HP1600M',        match => 'HP J4120A ProCurve Switch 1600M'      },
103   J9029A           => { type => 1, model => 'HP1800-8G',      match => 'PROCURVE J9029A'                      },
104   J9449A           => { type => 1, model => 'HP1810-8G',      match => 'HP ProCurve 1810G - 8 GE'             },
105   J4093A           => { type => 1, model => 'HP2424M',        match => 'HP J4093A ProCurve Switch 2424M'      },
106   J9279A           => { type => 1, model => 'HP2510G-24',     match => 'ProCurve J9279A Switch 2510G-24'      },
107   J9280A           => { type => 1, model => 'HP2510G-48',     match => 'ProCurve J9280A Switch 2510G-48'      },
108   J4813A           => { type => 1, model => 'HP2524',         match => 'HP J4813A ProCurve Switch 2524'       },
109   J4900A           => { type => 1, model => 'HP2626A',        match => 'HP J4900A ProCurve Switch 2626'       },
110   J4900B           => { type => 1, model => 'HP2626B',        match => 'J4900B.+?Switch 2626'                 }, # ProCurve J4900B Switch 2626 # HP J4900B ProCurve Switch 2626
111   J4899B           => { type => 1, model => 'HP2650',         match => 'ProCurve J4899B Switch 2650'          },
112   J9021A           => { type => 1, model => 'HP2810-24G',     match => 'ProCurve J9021A Switch 2810-24G'      },
113   J9022A           => { type => 1, model => 'HP2810-48G',     match => 'ProCurve J9022A Switch 2810-48G'      },
114   J8692A           => { type => 1, model => 'HP3500-24G',     match => 'J8692A Switch 3500yl-24G'             },
115   J4903A           => { type => 1, model => 'HP2824',         match => 'J4903A.+?Switch 2824,'                },
116   J4110A           => { type => 1, model => 'HP8000M',        match => 'HP J4110A ProCurve Switch 8000M'      },
117   JE074A           => { type => 2, model => 'HP5120-24G',     match => 'HP Comware.+?A5120-24G EI Switch'     },
118   JE069A           => { type => 2, model => 'HP5120-48G',     match => 'HP Comware.+?A5120-48G EI Switch'     },
119   JD377A           => { type => 2, model => 'HP5500-24G',     match => 'HP Comware.+?A5500-24G EI Switch'     },
120   JD374A           => { type => 2, model => 'HP5500-24F',     match => 'HP Comware.+?A5500-24G-SFP EI Switch' },
121   # BayStack
122   BS350T           => { type => 1, model => 'BS350T',         match => 'BayStack 350T HW'                     },
123   # Nexans
124   N3483G           => { type => 2, model => 'NA3483-6G',      match => 'GigaSwitch V3 TP SFP-I 48V ES3'       },
125   # DELL
126   PC7024           => { type => 2, model => 'DPC7024',        match => 'PowerConnect 7024,.+?VxWorks'         },
127   N2048            => { type => 2, model => 'DN2048',         match => 'Dell Networking N2048,'               },
128   N4032F           => { type => 2, model => 'DN4032F',        match => 'Dell Networking N4032F,'              },
129   N4064F           => { type => 2, model => 'DN4064F',        match => 'Dell Networking N4064F,'              },
130   # 3COM
131   'H3C5500'        => { type => 1, model => 'H3C5500',        match => 'H3C S5500-SI Series'                  },
132   '3C17203'        => { type => 1, model => '3C17203',        match => '3Com SuperStack 3 24-Port'            },
133   '3C17204'        => { type => 1, model => '3C17204',        match => '3Com SuperStack 3 48-Port'            },
134   '3CR17562-91'    => { type => 1, model => '3CR17562-91',    match => '3Com Switch 4500 50-Port'             },
135   '3CR17255-91'    => { type => 1, model => '3CR17255-91',    match => '3Com Switch 5500G-EI 48-Port'         },
136   '3CR17251-91'    => { type => 1, model => '3CR17251-91',    match => '3Com Switch 5500G-EI 48-Port'         },
137   '3CR17571-91'    => { type => 1, model => '3CR17571-91',    match => '3Com Switch 4500 PWR 26-Port'         },
138   '3CRWX220095A'   => { type => 1, model => '3CRWX220095A',   match => '3Com Wireless LAN Controller'         },
139   '3CR17254-91'    => { type => 1, model => '3CR17254-91',    match => '3Com Switch 5500G-EI 24-Port'         },
140   '3CRS48G-24S-91' => { type => 1, model => '3CRS48G-24S-91', match => '3Com Switch 4800G 24-Port'            },
141   '3CRS48G-48S-91' => { type => 1, model => '3CRS48G-48S-91', match => '3Com Switch 4800G 48-Port'            },
142   '3C17708'        => { type => 1, model => '3C17708',        match => '3Com Switch 4050'                     },
143   '3C17709'        => { type => 1, model => '3C17709',        match => '3Com Switch 4060'                     },
144   '3C17707'        => { type => 1, model => '3C17707',        match => '3Com Switch 4070'                     },
145   '3CR17258-91'    => { type => 1, model => '3CR17258-91',    match => '3Com Switch 5500G-EI 24-Port SFP'     },
146   '3CR17181-91'    => { type => 1, model => '3CR17181-91',    match => '3Com Switch 5500-EI 28-Port FX'       },
147   '3CR17252-91'    => { type => 1, model => '3CR17252-91',    match => '3Com Switch 5500G-EI PWR 24-Port'     },
148   '3CR17253-91'    => { type => 1, model => '3CR17253-91',    match => '3Com Switch 5500G-EI PWR 48-Port'     },
149   '3CR17250-91'    => { type => 1, model => '3CR17250-91',    match => '3Com Switch 5500G-EI 24-Port'         },
150   '3CR17561-91'    => { type => 1, model => '3CR17561-91',    match => '3Com Switch 4500 26-Port'             },
151   '3CR17572-91'    => { type => 1, model => '3CR17572-91',    match => '3Com Switch 4500 PWR 50-Port'         },
152   '3C17702-US'     => { type => 1, model => '3C17702-US',     match => '3Com Switch 4900 SX'                  },
153   '3C17700'        => { type => 1, model => '3C17700',        match => '3Com Switch 4900'                     },
154   );
155
156Readonly my %OID_NUMBER => (
157   sysDescription  => '1.3.6.1.2.1.1.1.0',
158   sysName         => '1.3.6.1.2.1.1.5.0',
159   sysContact      => '1.3.6.1.2.1.1.4.0',
160   sysLocation     => '1.3.6.1.2.1.1.6.0',
161   searchPort1     => '1.3.6.1.2.1.17.4.3.1.2',       # BRIDGE-MIB (802.1D).
162   searchPort2     => '1.3.6.1.2.1.17.7.1.2.2.1.2',   # Q-BRIDGE-MIB (802.1Q) add 0 if unknown vlan id
163   vlanPortDefault => '1.3.6.1.2.1.17.7.1.4.5.1.1',   # dot1qPvid
164   vlanStatus      => '1.3.6.1.2.1.17.7.1.4.3.1.5',   # integer 4 Create, 6 Destroy
165   vlanName        => '1.3.6.1.2.1.17.7.1.4.3.1.1',   # string
166   hpicfReset      => '1.3.6.1.4.1.11.2.14.11.1.4.1', # HP reboot switch
167   ifIndex         => '1.3.6.1.2.1.17.1.4.1.2',       # dot1dBasePortIfIndex - Interface index redirection
168   ifName          => '1.3.6.1.2.1.31.1.1.1.1',       # Interface name (give port number)
169   portUpDown      => '1.3.6.1.2.1.2.2.1.7',          # 1.3.6.1.2.1.2.2.1.7.NoPort = 1 (up)  = 2 (down)
170   );
171
172Readonly my %PORT_UPDOWN => (
173   1 => 'enable',
174   2 => 'disable',
175   );
176
177Readonly my $RE_MAC_ADDRESS  => qr{ [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} }xms;
178Readonly my $RE_IPv4_ADDRESS => qr{ [0-9]{1,3} \. [0-9]{1,3} \. [0-9]{1,3} \. [0-9]{1,3} }xms;
179
180Readonly my $RE_FLOAT_HOSTNAME => qr{ ^float }xms;
181
182################
183# main program
184################
185
186my $cmd = shift @ARGV || 'help';
187if (defined $CMD_DB{$cmd}) {
188   $CMD_DB{$cmd}->(@ARGV);
189   }
190else {
191   print {*STDERR} "klask: command $cmd not found\n\n";
192   $CMD_DB{help}->();
193   exit 1;
194   }
195
196exit;
197
198################
199# subroutine
200################
201
202sub test_running_environnement {
203   die "Configuration file $KLASK_CFG_FILE does not exists. Klask need it !\n" if not -e "$KLASK_CFG_FILE";
204   die "Var folder $KLASK_VAR does not exists. Klask need it !\n"              if not -d "$KLASK_VAR";
205   return;
206   }
207
208sub test_switchdb_environnement {
209   die "Switch database $KLASK_SW_FILE does not exists. Launch updatesw before this command !\n" if not -e "$KLASK_SW_FILE";
210   return;
211   }
212
213sub test_maindb_environnement {
214   die "Main database $KLASK_DB_FILE does not exists. Launch updatedb before this command !\n" if not -e "$KLASK_DB_FILE";
215   return;
216   }
217
218###
219# fast ping dont l'objectif est de remplir la table arp de la machine
220sub fast_ping {
221   # Launch this command without waiting...
222   system "fping -q -c 1 @_ >/dev/null 2>&1 &";
223   return;
224   }
225
226sub shell_command {
227   my $cmd = shift;
228
229   my $fh     = new FileHandle;
230   my $result = '';
231   open $fh, q{-|}, "LANG=C $cmd" or die "Can't exec $cmd\n";
232   $result .= <$fh>;
233   close $fh;
234   chomp $result;
235   return $result;
236   }
237
238###
239# donne l'@ ip, dns, arp en fonction du dns OU de l'ip
240sub resolve_ip_arp_host {
241   my $param_ip_or_host = shift;
242   my $interface = shift || q{*};
243   my $type      = shift || q{fast};
244   my $already   = shift || q{yes};
245
246   my %ret = (
247      hostname_fq  => 'unknow',
248      ipv4_address => '0.0.0.0',
249      mac_address  => 'unknow',
250      );
251
252   # perl -MSocket -E 'say inet_ntoa(scalar gethostbyname("tech7meylan.hmg.inpg.fr"))'
253   my $packed_ip = scalar gethostbyname($param_ip_or_host);
254   return %ret if not defined $packed_ip;
255   $ret{ipv4_address} = inet_ntoa($packed_ip);
256   #if ($ret{ipv4_address} !~ m/$RE_IPv4_ADDRESS/) {
257   #   print "Error: for computer $param_ip_or_host on interface $interface, IP $ret{ipv4_address} is not valide\n";
258   #   return %ret;
259   #   }
260
261   # perl -MSocket -E 'say scalar gethostbyaddr(inet_aton("194.254.66.240"), AF_INET)'
262   my $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET) if $already eq 'yes';
263   $ret{hostname_fq} = $hostname_fq if defined $hostname_fq;
264
265   # my $cmd = q{grep  -he '\b} . $param_ip_or_host . q{\b' } . "/var/lib/arpwatch/$interface.dat | sort -rn -k 3,3 | head -1";
266   #my $cmd = q{grep  -he '\b} . $ret{ipv4_address} . q{\b' } . "/var/lib/arpwatch/$interface.dat | sort -rn -k 3,3 | head -1";
267   my $cmd = q{grep  -He '\b} . $ret{ipv4_address} . q{\b' } . "/var/lib/arpwatch/$interface.dat" . '| sed -e \'s|^/var/lib/arpwatch/\(.*\)\.dat:|\1 |;\' | sort -rn -k 4,4 | head -1';
268   #grep -He 194.254.66.252 /var/lib/arpwatch/*.dat | sed -e 's|^/var/lib/arpwatch/\(.*\)\.dat:|\1\t|;' | sort -rn -k 4,4 | head -1
269
270   my $cmd_arpwatch = shell_command $cmd;
271   #my ($arp, $ip, $timestamp, $host) = split m/ \s+ /xms, $cmd_arpwatch;
272   my ($interface2, $arp, $ip, $timestamp, $host) = split m/ \s+ /xms, $cmd_arpwatch;
273
274   $ret{interface}    = $interface2 || $interface;
275   $ret{mac_address}  = $arp       if $arp;
276   $ret{timestamp}    = $timestamp if $timestamp;
277
278   my $nowtimestamp = time;
279
280   if ( $type eq 'fast' and ( not defined $timestamp or $timestamp < ( $nowtimestamp - 45 * 60 ) ) ) { # 45 min
281      $ret{mac_address} = 'unknow';
282      return %ret;
283      }
284
285   # ARP result
286   #
287   # LANG=C arp -a 194.254.66.62 -i eth331
288   # gw66-62.legi.grenoble-inp.fr (194.254.66.62) at 00:08:7c:bb:0f:c0 [ether] on eth331
289   #
290   # LANG=C ip neigh show to 194.254.66.62 dev eth331
291   # 194.254.66.62 lladdr 00:08:7c:bb:0f:c0 REACHABLE
292   # LANG=C ip neigh show to 194.254.66.62
293   # 194.254.66.62 dev eth331 lladdr 00:08:7c:bb:0f:c0 REACHABLE
294#   my $cmd_arp  = shell_command "arp -a $param_ip_or_host -i $ret{interface}";
295#   if ( $cmd_arp =~ m{ (\S*) \s \( ( $RE_IPv4_ADDRESS ) \) \s at \s ( $RE_MAC_ADDRESS ) }xms ) {
296#      ( $ret{hostname_fq}, $ret{ipv4_address}, $ret{mac_address} )  = ($1, $2, $3);
297#      }
298   if ($ret{mac_address} eq 'unknow') {
299      # Last chance to have the mac_address
300      if ($ret{interface} eq '*') {
301         my $cmd_arp  = shell_command "ip neigh show to $ret{ipv4_address}";
302         if ( $cmd_arp =~ m{ ^$RE_IPv4_ADDRESS \s dev \s ([\w\d\.\:]+) \s lladdr \s ( $RE_MAC_ADDRESS ) \s }xms ) {
303            ($ret{interface}, $ret{mac_address}) = ($1, $2);
304            }
305         }
306      else {
307         my $cmd_arp  = shell_command "ip neigh show to $ret{ipv4_address} dev $ret{interface}";
308         if ( $cmd_arp =~ m{ ^$RE_IPv4_ADDRESS \s lladdr \s ( $RE_MAC_ADDRESS ) \s }xms ) {
309            $ret{mac_address} = $1;
310            }
311         }
312      }
313
314   # Normalize MAC Address
315   if ($ret{mac_address} ne 'unknow') {
316      my @paquets = ();
317      for ( split m/ : /xms, $ret{mac_address} ) {
318         my @chars = split m//xms, uc "00$_";
319         push @paquets, "$chars[-2]$chars[-1]";
320         }
321      $ret{mac_address} = join q{:}, @paquets;
322      }
323
324   return %ret;
325   }
326
327# Find Surname of a switch
328sub get_switch_model {
329   my $sw_snmp_description = shift || 'unknow';
330   $sw_snmp_description =~ s/[\n\r]/ /g;
331
332   for my $sw_kind (keys %SWITCH_KIND) {
333      next if not $sw_snmp_description =~ m/$SWITCH_KIND{$sw_kind}->{match}/ms; # option xms break search, why ?
334
335      return $SWITCH_KIND{$sw_kind}->{model};
336      }
337
338   return $sw_snmp_description;
339   }
340
341###
342# va rechercher le nom des switchs pour savoir qui est qui
343sub init_switch_names {
344   my ($verbose, $verb_description, $check_hostname, $check_location) = @_;
345
346   printf "%-26s                %-25s %s\n",'Switch','Description','Type' if $verbose;
347   print "------------------------------------------------------------------------------\n" if $verbose;
348
349   INIT_EACH_SWITCH:
350   for my $sw (@SWITCH_LIST) {
351      my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
352      print "$error \n" if $error;
353
354      my $result = $session->get_request(
355         -varbindlist => [
356            $OID_NUMBER{sysDescription},
357            $OID_NUMBER{sysName},
358            $OID_NUMBER{sysContact},
359            $OID_NUMBER{sysLocation},
360            ]
361         );
362      if (!defined $result) {
363         printf {*STDERR} "ERROR: %s.\n", $session->error();
364         $session->close();
365         # Remove bad switch
366         @SWITCH_LIST = grep { $_->{hostname} ne $sw->{hostname} } @SWITCH_LIST;
367         delete $SWITCH_LEVEL{$sw->{hostname}} if exists $SWITCH_LEVEL{$sw->{hostname}};
368         delete $SWITCH_DB{$sw->{hostname}}    if exists $SWITCH_DB{$sw->{hostname}};
369         next INIT_EACH_SWITCH;
370         }
371
372      $sw->{description} = $result->{$OID_NUMBER{sysName}} || $sw->{hostname};
373      $sw->{model} = get_switch_model($result->{$OID_NUMBER{sysDescription}});
374      if ($verb_description) {
375         my $desc = $result->{$OID_NUMBER{sysDescription}};
376         $desc =~ s/[\n\r]/ /g;
377         print "   description: $desc\n"
378         }
379      if ($check_hostname) {
380         my ($hostname) = split /\./, $sw->{hostname}, 2;
381         print " $hostname - error internal hostname: $sw->{hostname}\n" if $result->{$OID_NUMBER{sysName}} ne $hostname;
382         }
383      if ($check_location) {
384         my $location = $result->{$OID_NUMBER{sysLocation}};
385         $location =~ s/^"(.+)"$/$1/;
386         print " $sw->{hostname} - error location: '$location' -> '$sw->{location}'\n" if $location ne $sw->{location};
387         }
388      #$sw->{location} = $result->{"1.3.6.1.2.1.1.6.0"} || $sw->{hostname};
389      #$sw->{contact} = $result->{"1.3.6.1.2.1.1.4.0"} || $sw->{hostname};
390      $session->close;
391
392      # Ligne à virer car on récupère maintenant le modèle du switch
393      #my ($desc, $type) = split m/ : /xms, $sw->{description}, 2;
394      printf "%-26s 0--------->>>> %-25s %s\n", $sw->{hostname}, $sw->{description}, $sw->{model} if $verbose;
395      }
396
397   print "\n" if $verbose;
398   return;
399   }
400
401###
402# convertit l'hexa (uniquement 2 chiffres) en decimal
403sub digit_hex_to_dec {
404   #00:0F:1F:43:E4:2B
405   my $car = '00' . uc shift;
406
407   return '00' if $car eq '00UNKNOW';
408   my %table = (
409      '0'=>'0',  '1'=>'1',  '2'=>'2',  '3'=>'3',  '4'=>'4',
410      '5'=>'5',  '6'=>'6',  '7'=>'7',  '8'=>'8',  '9'=>'9',
411      'A'=>'10', 'B'=>'11', 'C'=>'12', 'D'=>'13', 'E'=>'14', 'F'=>'15',
412      );
413   my @chars = split m//xms, $car;
414   return $table{$chars[-2]}*16 + $table{$chars[-1]};
415   }
416
417#--------------------------------------------------------------------------------
418
419sub normalize_mac_address {
420   my $mac_address = shift;
421
422   # D07E-28D1-7AB8 or d07e28-d17ab8
423   if ($mac_address =~ m{^ (?: [0-9A-Fa-f]{4} -){2} [0-9A-Fa-f]{4} $}xms
424      or $mac_address =~ m{^ [0-9A-Fa-f]{6} - [0-9A-Fa-f]{6} $}xms) {
425      $mac_address =~ s/-//g;
426      return join q{:}, unpack('(A2)*', uc($mac_address));
427      }
428
429   return join q{:}, map { substr( uc("00$_"), -2) } split m/ [:-] /xms, $mac_address;
430   }
431
432#--------------------------------------------------------------------------------
433# convertit l'@ mac en decimal
434sub mac_address_hex_to_dec {
435   #00:0F:1F:43:E4:2B
436   my $mac_address = shift;
437
438   my @paquets = split m/ : /xms, $mac_address;
439   my $return = q{};
440   for (@paquets) {
441      $return .= q{.} . digit_hex_to_dec($_);
442      }
443   return $return;
444   }
445
446###
447# va rechercher le port et le switch sur lequel est la machine
448sub find_switch_port {
449   my $mac_address     = shift;
450   my $switch_proposal = shift || q{};
451   my $vlan_id = shift || 0;
452
453   my %ret;
454   $ret{switch_description} = 'unknow';
455   $ret{switch_port} = '0';
456
457   return %ret if $mac_address eq 'unknow';;
458
459   my @switch_search = @SWITCH_LIST;
460   if ($switch_proposal ne q{}) {
461      for my $sw (@SWITCH_LIST) {
462         next if $sw->{hostname} ne $switch_proposal;
463         unshift @switch_search, $sw;
464         last;
465         }
466      }
467
468   my $research1 = $OID_NUMBER{searchPort1} . mac_address_hex_to_dec($mac_address);
469   my $research2 = $OID_NUMBER{searchPort2} .'.'. $vlan_id . mac_address_hex_to_dec($mac_address);
470
471   LOOP_ON_SWITCH:
472   for my $sw (@switch_search) {
473      my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
474      print "$error \n" if $error;
475
476      my $result = $session->get_request(
477         -varbindlist => [$research1]
478         );
479      if (not defined $result) {
480         $result = $session->get_request(
481            -varbindlist => [$research2]
482            );
483         $result->{$research1} = $result->{$research2} if defined $result;
484         }
485
486      if (not (defined $result and $result->{$research1} ne 'noSuchInstance')) {
487         $session->close;
488         next LOOP_ON_SWITCH;
489         }
490
491      my $swport_num = $result->{$research1};
492      my $swport_hr = get_human_readable_port($sw->{model}, snmp_get_switchport_hr($session, $swport_num));
493
494      $session->close;
495
496      # IMPORTANT !!
497      # ceci empeche la detection sur certains port ...
498      # en effet les switch sont relies entre eux par un cable reseau et du coup
499      # tous les arp de toutes les machines sont presentes sur ces ports (ceux choisis ici sont les miens)
500      # cette partie est a ameliore, voir a configurer dans l'entete
501      # 21->24 45->48
502      SWITCH_PORT_IGNORE:
503      for my $portignore (@{$sw->{portignore}}) {
504         next LOOP_ON_SWITCH if $swport_hr eq $portignore;
505         }
506
507      $ret{switch_hostname}    = $sw->{hostname};
508      $ret{switch_description} = $sw->{description};
509      $ret{switch_port}        = $swport_num;
510      $ret{switch_port_hr}     = $swport_hr; # human readable
511
512      last LOOP_ON_SWITCH;
513      }
514   return %ret;
515   }
516
517###
518# va rechercher les port et les switch sur lequel est la machine
519sub find_all_switch_port {
520   my $mac_address = shift;
521   my $vlan_id     = shift || 0;
522
523   my $ret = {};
524
525   return $ret if $mac_address eq 'unknow';
526
527   my $research1 = $OID_NUMBER{searchPort1} . mac_address_hex_to_dec($mac_address);
528   my $research2 = $OID_NUMBER{searchPort2} .'.'. $vlan_id . mac_address_hex_to_dec($mac_address);
529   LOOP_ON_ALL_SWITCH:
530   for my $sw (@SWITCH_LIST) {
531      my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
532      print "$error \n" if $error;
533
534      my $result = $session->get_request(
535         -varbindlist => [$research1]
536         );
537      if (not defined $result) {
538         $result = $session->get_request(
539            -varbindlist => [$research2]
540            );
541         $result->{$research1} = $result->{$research2} if defined $result;
542         }
543
544      if (defined $result and $result->{$research1} ne 'noSuchInstance') {
545         my $swport_num = $result->{$research1};
546         my $swport_hr = get_human_readable_port($sw->{model}, snmp_get_switchport_hr($session, $swport_num));
547
548         SWITCH_PORT_IGNORE:
549         for my $portignore (@{$sw->{portignore}}) {
550            if ($swport_hr eq $portignore) {
551               $session->close;
552               next LOOP_ON_ALL_SWITCH
553               }
554            }
555
556         $ret->{$sw->{hostname}} = {};
557         $ret->{$sw->{hostname}}{hostname}    = $sw->{hostname};
558         $ret->{$sw->{hostname}}{description} = $sw->{description};
559         $ret->{$sw->{hostname}}{port}        = $swport_num;
560         $ret->{$sw->{hostname}}{port_hr}     = $swport_hr;
561
562#         $SWITCH_PORT_COUNT{$sw->{hostname}}->{$swport}++;
563         }
564
565      $session->close;
566      }
567   return $ret;
568   }
569
570sub get_list_network {
571
572   return keys %{$KLASK_CFG->{network}};
573   }
574
575sub get_current_interface {
576   my $vlan_name = shift;
577
578   return $KLASK_CFG->{network}{$vlan_name}{interface};
579   }
580
581sub get_current_vlan_id {
582   my $vlan_name = shift;
583
584   return 0 if not exists $KLASK_CFG->{network}{$vlan_name};
585   return $KLASK_CFG->{network}{$vlan_name}{'vlan-id'};
586   }
587
588sub get_current_scan_mode {
589   my $vlan_name = shift;
590
591   return $KLASK_CFG->{network}{$vlan_name}{'scan-mode'} || $DEFAULT{'scan-mode'} || 'active';
592   }
593
594sub get_current_vlan_name_for_interface {
595   my $interface = shift;
596
597   for my $vlan_name (keys %{$KLASK_CFG->{network}}) {
598      next if $KLASK_CFG->{network}{$vlan_name}{interface} ne $interface;
599      return $vlan_name;
600      }
601   }
602
603###
604# liste l'ensemble des adresses ip d'un réseau
605sub get_list_ip {
606   my @vlan_name = @_;
607
608   my $cidrlist = Net::CIDR::Lite->new;
609
610   for my $net (@vlan_name) {
611      my @line  = @{$KLASK_CFG->{network}{$net}{'ip-subnet'}};
612      for my $cmd (@line) {
613         for my $method (keys %{$cmd}) {
614            $cidrlist->add_any($cmd->{$method}) if $method eq 'add';
615            }
616         }
617      }
618
619   my @res = ();
620
621   for my $cidr ($cidrlist->list()) {
622      my $net = new NetAddr::IP $cidr;
623      for my $ip (@{$net}) {
624         $ip =~ s{ /32 }{}xms;
625         push @res,  $ip;
626         }
627      }
628
629   return @res;
630   }
631
632# liste l'ensemble des routeurs du réseau
633sub get_list_main_router {
634   my @vlan_name = @_;
635
636   my @res = ();
637
638   for my $net (@vlan_name) {
639      push @res, $KLASK_CFG->{network}{$net}{'main-router'};
640      }
641
642   return @res;
643   }
644
645sub get_human_readable_port {
646   my $sw_model = shift;
647   my $sw_port  = shift;
648
649   # Not need anymore
650   # get port name by snmp
651   return $sw_port;
652
653   if ($sw_model eq 'HP8000M') {
654
655      my $reste = (($sw_port - 1) % 8) + 1;
656      my $major = int (($sw_port - 1) / 8);
657      return "$INTERNAL_PORT_MAP{$major}$reste";
658      }
659
660   if ($sw_model eq 'HP2424M') {
661      if ($sw_port > 24) {
662
663         my $reste = $sw_port - 24;
664         return "A$reste";
665         }
666      }
667
668   if ($sw_model eq 'HP1600M') {
669      if ($sw_port > 16) {
670
671         my $reste = $sw_port - 16;
672         return "A$reste";
673         }
674      }
675
676   if ($sw_model eq 'HP2810-48G' or $sw_model eq 'HP2810-24G') {
677      if ($sw_port > 48) {
678
679         my $reste = $sw_port - 48;
680         return "Trk$reste";
681         }
682      }
683
684   if ($sw_model eq 'HP3500-24G') {
685      if ($sw_port > 289) {
686
687         my $reste = $sw_port - 289;
688         return "Trk$reste";
689         }
690      }
691
692   return $sw_port;
693   }
694
695sub get_numerical_port {
696   my $sw_model = shift;
697   my $sw_port  = shift;
698
699   if ($sw_model eq 'HP8000M') {
700
701      my $letter = substr $sw_port, 0, 1;
702      my $reste =  substr $sw_port, 1;
703
704      return $INTERNAL_PORT_MAP_REV{$letter} * 8 + $reste;
705      }
706
707   if ($sw_model eq 'HP2424M') {
708      if ($sw_port =~ m/^A/xms ) {
709
710         my $reste =  substr $sw_port, 1;
711
712         return 24 + $reste;
713         }
714      }
715
716   if ($sw_model eq 'HP1600M') {
717      if ($sw_port =~ m/^A/xms ) {
718
719         my $reste =  substr $sw_port, 1;
720
721         return 16 + $reste;
722         }
723      }
724
725   if ($sw_model eq 'HP2810-48G' or $sw_model eq 'HP2810-24G') {
726      if ($sw_port =~ m/^Trk/xms ) {
727
728         my $reste =  substr $sw_port, 3;
729
730         return 48 + $reste;
731         }
732      }
733
734   if ($sw_model eq 'HP3500-24G') {
735      if ($sw_port =~ m/^Trk/xms ) {
736
737         my $reste =  substr $sw_port, 3;
738
739         return 289 + $reste;
740         }
741      }
742
743   return $sw_port;
744   }
745
746sub normalize_port_human_readable {
747   my $sw_port_hr  = shift;
748
749   # Manufacturer abbreviation
750   $sw_port_hr =~ s/^Bridge-Aggregation/Br/i;
751   $sw_port_hr =~ s/^Port-Channel/Po/i;
752   $sw_port_hr =~ s/^Forty-?GigabitEthernet/Fo/i;
753   $sw_port_hr =~ s/^Ten-?GigabitEthernet/Te/i;
754   $sw_port_hr =~ s/^GigabitEthernet/Gi/i;
755   $sw_port_hr =~ s/^FastEthernet/Fa/i;
756
757   # Customer abbreviation
758   $sw_port_hr =~ s/^Ten/Te/i;
759   $sw_port_hr =~ s/^Giga/Gi/i;
760
761   return ucfirst $sw_port_hr;
762   }
763
764sub snmp_get_switchport_hr {
765   my ($snmp_session, $swport) = @_;
766
767   my $research_index = $OID_NUMBER{ifIndex} .'.'. $swport;
768   my $result_index = $snmp_session->get_request(
769      -varbindlist => [$research_index]
770      );
771   my $swifindex = $swport;
772   $swifindex = $result_index->{$research_index} if defined $result_index;
773
774   my $research_hr = $OID_NUMBER{ifName} .'.'. $swifindex;
775   my $result_hr = $snmp_session->get_request(
776      -varbindlist => [$research_hr]
777      );
778   my $swport_hr = $swport;
779   $swport_hr = normalize_port_human_readable($result_hr->{$research_hr}) if defined $result_hr;
780   return $swport_hr;
781   }
782
783# Reverse search port number
784sub snmp_get_switchport_num {
785   my ($snmp_session, $swport_hr, $verbose) = @_;
786
787   my $swport_num = $swport_hr;
788   return $swport_num if $swport_num =~ m/^\d+$/; # direct return if already numeric
789
790   my $research_index = $OID_NUMBER{ifIndex}; # base OID
791   my @args = ( -varbindlist =>  [$research_index]);
792   LOOP_OID_PORT:
793   while ( defined $snmp_session->get_next_request(@args) ) {
794      my ($oid_current) = $snmp_session->var_bind_names;
795      last if  not Net::SNMP::oid_base_match($research_index, $oid_current);
796     
797      my $port_ifIndex = $snmp_session->var_bind_list->{$oid_current};
798      my ($port_index) = reverse split /\./, $oid_current; # last number
799      printf "PORT1: %s => %s\n", $oid_current, $port_ifIndex if $verbose;
800
801      # prepare next loop
802      @args = (-varbindlist => [$oid_current]);
803     
804      my $oid_ifName = $OID_NUMBER{ifName} .'.'. $port_ifIndex;
805      my $result = $snmp_session->get_request(-varbindlist => [$oid_ifName]);
806      next LOOP_OID_PORT if not defined $result;
807     
808      my $current_port_hr = normalize_port_human_readable($result->{$oid_ifName});
809      printf "PORT2: $oid_ifName => $current_port_hr\n" if $verbose;
810      if ($current_port_hr eq $swport_hr) {
811         print "PORT3: $current_port_hr <-> $port_index\n" if $verbose;
812         $swport_num = $port_index;
813         last LOOP_OID_PORT;
814         }
815      }
816   return $swport_num;
817   }
818
819#   $research_index = $OID_NUMBER{ifName}; # base OID
820#   @args = ( -varbindlist =>  [$research_index]);
821#   while ( defined $snmp_session->get_next_request(@args) ) {
822#      my ($oid_current) = $snmp_session->var_bind_names;
823#      last if not Net::SNMP::oid_base_match($research_index, $oid_current);
824#      my $current_port_hr = normalize_port_human_readable($snmp_session->var_bind_list->{$oid_current});
825 #     printf "%s => %s\n", $oid_current, $current_port_hr;  # if $verbose;
826#      if ( $current_port_hr =~ m/$swport_hr/i ) {
827#         print "FIND PORT $current_port_hr  -- $swport_h\n";
828#         last
829#         }
830#      @args = (-varbindlist => [$oid_current]);
831#      }
832#   return;
833
834#   my $result_index = $snmp_session->get_bulk_request(
835#      -maxrepetitions   => 10,
836#      -varbindlist => [$research_index]
837 #     );
838#   my $swifindex = $swport;
839#   $swifindex = $result_index->{$research_index} if defined $result_index;
840#
841#   my $research_hr = $OID_NUMBER{ifName} .'.'. $swifindex;
842#   my $result_hr = $snmp_session->get_request(
843#      -varbindlist => [$research_hr]
844#      );
845#   my $swport_hr = $swport;
846#   $swport_hr = normalize_port_human_readable($result_hr->{$research_hr}) if defined $result_hr;
847#   return $swport_hr;
848#   }
849
850# Load computer database
851sub computerdb_load {
852   my $computerdb = YAML::Syck::LoadFile("$KLASK_DB_FILE");
853
854   LOOP_ON_IP_ADDRESS:
855   for my $ip (keys %{$computerdb}) {
856
857      next LOOP_ON_IP_ADDRESS if exists $computerdb->{$ip}{switch_port_hr} and defined $computerdb->{$ip}{switch_port_hr};
858
859      $computerdb->{$ip}{switch_port_hr} = $computerdb->{$ip}{switch_port};
860      }
861
862   return $computerdb;
863   }
864
865################
866# Les commandes
867################
868
869sub cmd_help {
870
871print <<'END';
872klask - ports manager and finder for switch
873
874 klask version
875
876 klask updatedb
877 klask exportdb --format [txt|html]
878 klask removedb computer*
879 klask cleandb  --day number_of_day --repair-dns --verbose
880
881 klask updatesw
882 klask exportsw --format [txt|dot]
883
884 klask searchdb computer
885 klask search   computer
886 klask search-mac-on-switch switch mac_addr
887
888 klask ip-free --day number_of_day --format [txt|html] [vlan_name]
889
890 klask bad-vlan-id
891
892 klask enable  switch port
893 klask disable switch port
894 klask status  switch port
895END
896   return;
897   }
898
899sub cmd_version {
900
901print <<'END';
902Klask - ports manager and finder for switch
903Copyright (C) 2005-2017 Gabriel Moreau
904
905END
906   print ' $Id: klask 215 2017-02-18 13:25:55Z g7moreau $'."\n";
907   return;
908   }
909
910sub cmd_search {
911   my @computer = @_;
912
913   init_switch_names();    #nomme les switchs
914   fast_ping(@computer);
915
916   LOOP_ON_COMPUTER:
917   for my $clientname (@computer) {
918      my %resol_arp = resolve_ip_arp_host($clientname);          #resolution arp
919      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{interface});
920      my $vlan_id   = get_current_vlan_id($vlan_name);
921      my %where     = find_switch_port($resol_arp{mac_address}, '', $vlan_id); #retrouve l'emplacement
922
923      next LOOP_ON_COMPUTER if $where{switch_description} eq 'unknow' or $resol_arp{hostname_fq} eq 'unknow' or $resol_arp{mac_address} eq 'unknow';
924
925      printf '%-22s %2s %-30s %-15s %18s',
926         $where{switch_hostname},
927         $where{switch_port_hr},
928         $resol_arp{hostname_fq},
929         $resol_arp{ipv4_address},
930         $resol_arp{mac_address}."\n";
931      }
932   return;
933   }
934
935sub cmd_searchdb {
936   my @ARGV  = @_;
937
938   my $kind;
939
940   GetOptions(
941      'kind=s'   => \$kind,
942      );
943
944   my %possible_search = (
945      host  => \&cmd_searchdb_host,
946      mac   => \&cmd_searchdb_mac,
947      );
948
949   $kind = 'host' if not defined $possible_search{$kind};
950
951   $possible_search{$kind}->(@ARGV);
952   return;
953   }
954
955
956sub cmd_searchdb_host {
957   my @computer = @_;
958
959   fast_ping(@computer);
960   my $computerdb = computerdb_load();
961
962   LOOP_ON_COMPUTER:
963   for my $clientname (@computer) {
964      my %resol_arp = resolve_ip_arp_host($clientname);      #resolution arp
965      my $ip = $resol_arp{ipv4_address};
966
967      next LOOP_ON_COMPUTER unless exists $computerdb->{$ip};
968
969      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
970      $year += 1900;
971      $mon++;
972      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
973
974      printf "%-22s %2s %-30s %-15s %-18s %s\n",
975         $computerdb->{$ip}{switch_hostname},
976         $computerdb->{$ip}{switch_port_hr},
977         $computerdb->{$ip}{hostname_fq},
978         $ip,
979         $computerdb->{$ip}{mac_address},
980         $date;
981      }
982   return;
983   }
984
985sub cmd_searchdb_mac {
986   my @mac = map { normalize_mac_address($_) } @_;
987
988   my $computerdb = computerdb_load();
989
990   LOOP_ON_MAC:
991   for my $mac (@mac) {
992      LOOP_ON_COMPUTER:
993      for my $ip (keys %{$computerdb}) {
994         next LOOP_ON_COMPUTER if $mac ne $computerdb->{$ip}{mac_address};
995
996         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
997         $year += 1900;
998         $mon++;
999         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1000
1001         printf "%-22s %2s %-30s %-15s %-18s %s\n",
1002            $computerdb->{$ip}{switch_hostname},
1003            $computerdb->{$ip}{switch_port_hr},
1004            $computerdb->{$ip}{hostname_fq},
1005            $ip,
1006            $computerdb->{$ip}{mac_address},
1007            $date;
1008         #next LOOP_ON_MAC;
1009         }
1010
1011      }
1012   return;
1013   }
1014
1015sub cmd_updatedb {
1016   @ARGV = @_;
1017
1018   my ($verbose, $verb_description, $check_hostname, $check_location);
1019
1020   GetOptions(
1021      'verbose|v'          => \$verbose,
1022      'verb-description|d' => \$verb_description,
1023      'chk-hostname|h'     => \$check_hostname,
1024      'chk-location|l'     => \$check_location,
1025      );
1026
1027   my @network = @ARGV;
1028      @network = get_list_network() if not @network;
1029
1030   test_switchdb_environnement();
1031
1032   my $computerdb = {};
1033      $computerdb = computerdb_load() if -e "$KLASK_DB_FILE";
1034   my $timestamp = time;
1035
1036   my %computer_not_detected = ();
1037   my $timestamp_last_week = $timestamp - (3600 * 24 * 7);
1038
1039   my $number_of_computer = get_list_ip(@network); # + 1;
1040   my $size_of_database   = keys %{$computerdb};
1041      $size_of_database   = 1 if $size_of_database == 0;
1042   my $i = 0;
1043   my $detected_computer = 0;
1044
1045   init_switch_names('yes', $verb_description, $check_hostname, $check_location);    #nomme les switchs
1046
1047   { # Remplis le champs portignore des ports d'inter-connection pour chaque switch
1048   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
1049   my %db_switch_output_port       = %{$switch_connection->{output_port}};
1050   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};
1051   my %db_switch_chained_port = ();
1052   for my $swport (keys %db_switch_connected_on_port) {
1053      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
1054      $db_switch_chained_port{$sw_connect} .= "$port_connect:";
1055      }
1056   for my $sw (@SWITCH_LIST) {
1057      push @{$sw->{portignore}}, $db_switch_output_port{$sw->{hostname}}  if exists $db_switch_output_port{$sw->{hostname}};
1058      if ( exists $db_switch_chained_port{$sw->{hostname}} ) {
1059         chop $db_switch_chained_port{$sw->{hostname}};
1060         push @{$sw->{portignore}}, split m/ : /xms, $db_switch_chained_port{$sw->{hostname}};
1061         }
1062#      print "$sw->{hostname} ++ @{$sw->{portignore}}\n";
1063      }
1064   }
1065
1066   my %router_mac_ip = ();
1067   DETECT_ALL_ROUTER:
1068#   for my $one_router ('194.254.66.254') {
1069   for my $one_router ( get_list_main_router(@network) ) {
1070      my %resol_arp = resolve_ip_arp_host($one_router);
1071      $router_mac_ip{ $resol_arp{mac_address} } = $resol_arp{ipv4_address};
1072      }
1073
1074   ALL_NETWORK:
1075   for my $current_net (@network) {
1076
1077      my @computer = get_list_ip($current_net);
1078      my $current_interface = get_current_interface($current_net);
1079
1080      fast_ping(@computer) if get_current_scan_mode($current_net) eq 'active';
1081
1082      LOOP_ON_COMPUTER:
1083      for my $one_computer (@computer) {
1084         $i++;
1085
1086         my $total_percent = int (($i*100)/$number_of_computer);
1087
1088         my $localtime = time - $timestamp;
1089         my ($sec,$min) = localtime $localtime;
1090
1091         my $time_elapse = 0;
1092            $time_elapse = $localtime * ( 100 - $total_percent) / $total_percent if $total_percent != 0;
1093         my ($sec_elapse,$min_elapse) = localtime $time_elapse;
1094
1095         printf "\rComputer scanned: %4i/%i (%2i%%)",  $i,                 $number_of_computer, $total_percent;
1096         printf ', detected: %4i/%i (%2i%%)', $detected_computer, $size_of_database,   int(($detected_computer*100)/$size_of_database);
1097         printf ' [Time: %02i:%02i / %02i:%02i]', int($localtime/60), $localtime % 60, int($time_elapse/60), $time_elapse % 60;
1098         printf ' %-8s %-14s', $current_interface, $one_computer;
1099
1100         my $already_exist = exists $computerdb->{$one_computer} ? 'yes' : 'no';
1101         my %resol_arp = resolve_ip_arp_host($one_computer, $current_interface, 'fast', $already_exist);
1102
1103         # do not search on router connection (why ?)
1104         if ( exists $router_mac_ip{$resol_arp{mac_address}}) {
1105            $computer_not_detected{$one_computer} = $current_net;
1106            next LOOP_ON_COMPUTER;
1107            }
1108
1109         # do not search on switch inter-connection
1110         if (exists $SWITCH_LEVEL{$resol_arp{hostname_fq}}) {
1111            $computer_not_detected{$one_computer} = $current_net;
1112            next LOOP_ON_COMPUTER;
1113            }
1114
1115         my $switch_proposal = q{};
1116         if (exists $computerdb->{$resol_arp{ipv4_address}} and exists $computerdb->{$resol_arp{ipv4_address}}{switch_hostname}) {
1117            $switch_proposal = $computerdb->{$resol_arp{ipv4_address}}{switch_hostname};
1118            }
1119
1120         # do not have a mac address
1121         if ($resol_arp{mac_address} eq 'unknow' or (exists $resol_arp{timestamps} and $resol_arp{timestamps} < ($timestamp - 3 * 3600))) {
1122            $computer_not_detected{$one_computer} = $current_net;
1123            next LOOP_ON_COMPUTER;
1124            }
1125
1126         my $vlan_name = get_current_vlan_name_for_interface($resol_arp{interface});
1127         my $vlan_id   = get_current_vlan_id($vlan_name);
1128         my %where = find_switch_port($resol_arp{mac_address},$switch_proposal,$vlan_id);
1129
1130         #192.168.24.156:
1131         #  arp: 00:0B:DB:D5:F6:65
1132         #  hostname: pcroyon.hmg.priv
1133         #  port: 5
1134         #  switch: sw-batH-legi:hp2524
1135         #  timestamp: 1164355525
1136
1137         # do not have a mac address
1138#         if ($resol_arp{mac_address} eq 'unknow') {
1139#            $computer_not_detected{$one_computer} = $current_interface;
1140#            next LOOP_ON_COMPUTER;
1141#            }
1142
1143         # detected on a switch
1144         if ($where{switch_description} ne 'unknow') {
1145            $detected_computer++;
1146            $computerdb->{$resol_arp{ipv4_address}} = {
1147               hostname_fq        => $resol_arp{hostname_fq},
1148               mac_address        => $resol_arp{mac_address},
1149               switch_hostname    => $where{switch_hostname},
1150               switch_description => $where{switch_description},
1151               switch_port        => $where{switch_port},
1152               switch_port_hr     => $where{switch_port_hr},
1153               timestamp          => $timestamp,
1154               network            => $current_net,
1155               };
1156            next LOOP_ON_COMPUTER;
1157            }
1158
1159         # new in the database but where it is ?
1160         if (not exists $computerdb->{$resol_arp{ipv4_address}}) {
1161            $detected_computer++;
1162            $computerdb->{$resol_arp{ipv4_address}} = {
1163               hostname_fq        => $resol_arp{hostname_fq},
1164               mac_address        => $resol_arp{mac_address},
1165               switch_hostname    => $where{switch_hostname},
1166               switch_description => $where{switch_description},
1167               switch_port        => $where{switch_port},
1168               switch_port_hr     => $where{switch_port_hr},
1169               timestamp          => $resol_arp{timestamp},
1170               network            => $current_net,
1171               };
1172            }
1173
1174         # mise a jour du nom de la machine si modification dans le dns
1175         $computerdb->{$resol_arp{ipv4_address}}{hostname_fq} = $resol_arp{hostname_fq};
1176
1177         # mise à jour de la date de détection si détection plus récente par arpwatch
1178         $computerdb->{$resol_arp{ipv4_address}}{timestamp}   = $resol_arp{timestamp} if exists $resol_arp{timestamp} and $computerdb->{$resol_arp{ipv4_address}}{timestamp} < $resol_arp{timestamp};
1179
1180         # relance un arping sur la machine si celle-ci n'a pas été détectée depuis plus d'une semaine
1181#         push @computer_not_detected, $resol_arp{ipv4_address} if $computerdb->{$resol_arp{ipv4_address}}{timestamp} < $timestamp_last_week;
1182         $computer_not_detected{$resol_arp{ipv4_address}} = $current_net if $computerdb->{$resol_arp{ipv4_address}}{timestamp} < $timestamp_last_week;
1183
1184         }
1185      }
1186
1187   # final end of line at the end of the loop
1188   printf "\n";
1189
1190   my $dirdb = $KLASK_DB_FILE;
1191      $dirdb =~ s{ / [^/]* $}{}xms;
1192   mkdir "$dirdb", 0755 unless -d "$dirdb";
1193   YAML::Syck::DumpFile("$KLASK_DB_FILE", $computerdb);
1194
1195   for my $one_computer (keys %computer_not_detected) {
1196      my $current_net = $computer_not_detected{$one_computer};
1197      my $current_interface = get_current_interface($current_net);
1198      system "arping -c 1 -w 1 -rR -i $current_interface $one_computer &>/dev/null" if get_current_scan_mode($current_net) eq 'active';
1199#      print  "arping -c 1 -w 1 -rR -i $current_interface $one_computer 2>/dev/null\n";
1200      }
1201   return;
1202   }
1203
1204sub cmd_removedb {
1205   my @computer = @_;
1206
1207   test_maindb_environnement();
1208
1209   my $computerdb = computerdb_load();
1210
1211   LOOP_ON_COMPUTER:
1212   for my $one_computer (@computer) {
1213
1214      if ( $one_computer =~ m/^ $RE_IPv4_ADDRESS $/xms
1215            and exists $computerdb->{$one_computer} ) {
1216         delete $computerdb->{$one_computer};
1217         next;
1218         }
1219
1220      my %resol_arp = resolve_ip_arp_host($one_computer);
1221
1222      delete $computerdb->{$resol_arp{ipv4_address}} if exists $computerdb->{$resol_arp{ipv4_address}};
1223      }
1224
1225   my $dirdb = $KLASK_DB_FILE;
1226      $dirdb =~ s{ / [^/]* $}{}xms;
1227   mkdir "$dirdb", 0755 unless -d "$dirdb";
1228   YAML::Syck::DumpFile("$KLASK_DB_FILE", $computerdb);
1229   return;
1230   }
1231
1232sub cmd_cleandb {
1233   my @ARGV  = @_;
1234
1235   my $days_to_clean = 15;
1236   my $repairdns;
1237   my $verbose;
1238   my $database_has_changed;
1239
1240   GetOptions(
1241      'day|d=i'   => \$days_to_clean,
1242      'verbose|v' => \$verbose,
1243      'repair-dns|r' => \$repairdns,
1244      );
1245
1246   my @vlan_name = get_list_network();
1247
1248   my $computerdb = computerdb_load();
1249   my $timestamp = time;
1250
1251   my $timestamp_barrier = 3600 * 24 * $days_to_clean;
1252   my $timestamp_3month  = 3600 * 24 * 90;
1253
1254   my %mactimedb = ();
1255   ALL_VLAN:
1256   for my $vlan (shuffle @vlan_name) {
1257
1258      my @ip_list   = shuffle get_list_ip($vlan);
1259
1260      LOOP_ON_IP_ADDRESS:
1261      for my $ip (@ip_list) {
1262
1263         next LOOP_ON_IP_ADDRESS if
1264            not exists $computerdb->{$ip};
1265
1266            #&& $computerdb->{$ip}{timestamp} > $timestamp_barrier;
1267         my $ip_timestamp   = $computerdb->{$ip}{timestamp};
1268         my $ip_mac         = $computerdb->{$ip}{mac_address};
1269         my $ip_hostname_fq = $computerdb->{$ip}{hostname_fq};
1270
1271         $mactimedb{$ip_mac} ||= {
1272            ip          => $ip,
1273            timestamp   => $ip_timestamp,
1274            vlan        => $vlan,
1275            hostname_fq => $ip_hostname_fq,
1276            };
1277
1278         if (
1279            ( $mactimedb{$ip_mac}->{timestamp} - $ip_timestamp > $timestamp_barrier
1280               or (
1281                  $mactimedb{$ip_mac}->{timestamp} > $ip_timestamp
1282                  and $timestamp - $mactimedb{$ip_mac}->{timestamp} > $timestamp_3month
1283                  )
1284            )
1285            and (
1286               not $mactimedb{$ip_mac}->{hostname_fq} =~ m/$RE_FLOAT_HOSTNAME/
1287               or $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/
1288               )) {
1289            print "remove ip $ip\n" if $verbose;
1290            delete $computerdb->{$ip};
1291            $database_has_changed++;
1292            }
1293
1294         elsif (
1295            ( $ip_timestamp - $mactimedb{$ip_mac}->{timestamp} > $timestamp_barrier
1296               or (
1297                  $ip_timestamp > $mactimedb{$ip_mac}->{timestamp}
1298                  and $timestamp - $ip_timestamp > $timestamp_3month
1299                  )
1300            )
1301            and (
1302               not $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/
1303               or $mactimedb{$ip_mac}->{hostname_fq} =~ m/$RE_FLOAT_HOSTNAME/
1304               )) {
1305            print "remove ip ".$mactimedb{$ip_mac}->{ip}."\n" if $verbose;
1306            delete $computerdb->{$mactimedb{$ip_mac}->{ip}};
1307            $database_has_changed++;
1308            }
1309
1310         if ( $ip_timestamp > $mactimedb{$ip_mac}->{timestamp}) {
1311            $mactimedb{$ip_mac} = {
1312               ip          => $ip,
1313               timestamp   => $ip_timestamp,
1314               vlan        => $vlan,
1315               hostname_fq => $ip_hostname_fq,
1316               };
1317            }
1318         }
1319      }
1320
1321   if ($repairdns) { # Search and update unkown computer in reverse DNS
1322      LOOP_ON_IP_ADDRESS:
1323      for my $ip (keys %{$computerdb}) {
1324         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} ne 'unknow';
1325
1326         my $packed_ip = scalar gethostbyname($ip);
1327         next LOOP_ON_IP_ADDRESS if not defined $packed_ip;
1328
1329         my $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET);
1330         next LOOP_ON_IP_ADDRESS if not defined $hostname_fq;
1331
1332         $computerdb->{$ip}{hostname_fq} = $hostname_fq;
1333         $database_has_changed++;
1334         }
1335      }
1336
1337   if ( $database_has_changed ) {
1338      my $dirdb = $KLASK_DB_FILE;
1339         $dirdb =~ s{ / [^/]* $}{}xms;
1340      mkdir "$dirdb", 0755 unless -d "$dirdb";
1341      YAML::Syck::DumpFile("$KLASK_DB_FILE", $computerdb);
1342      }
1343   return;
1344   }
1345
1346sub cmd_exportdb {
1347   @ARGV = @_;
1348
1349   my $format = 'txt';
1350
1351   GetOptions(
1352      'format|f=s'  => \$format,
1353      );
1354
1355   my %possible_format = (
1356      txt  => \&cmd_exportdb_txt,
1357      html => \&cmd_exportdb_html,
1358      );
1359
1360   $format = 'txt' if not defined $possible_format{$format};
1361
1362   $possible_format{$format}->(@ARGV);
1363   return;
1364   }
1365
1366sub cmd_exportdb_txt {
1367   test_maindb_environnement();
1368
1369   my $computerdb = computerdb_load();
1370
1371   printf "%-27s %-4s            %-40s %-15s %-18s %-16s %s\n", qw(Switch Port Hostname-FQ IPv4-Address MAC-Address Date VLAN);
1372   print "--------------------------------------------------------------------------------------------------------------------------------------------\n";
1373
1374   LOOP_ON_IP_ADDRESS:
1375   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
1376
1377#      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq 'unknow';
1378
1379      # to be improve in the future
1380      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !
1381
1382# dans le futur
1383#      next if $computerdb->{$ip}{hostname_fq} eq 'unknow';
1384
1385      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
1386      $year += 1900;
1387      $mon++;
1388      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1389
1390      my $vlan = '';
1391      $vlan = $computerdb->{$ip}{network}.'('.get_current_vlan_id($computerdb->{$ip}{network}).')' if $computerdb->{$ip}{network};
1392
1393      printf "%-28s  %2s  <-------  %-40s %-15s %-18s %-16s %s\n",
1394         $computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description},
1395         $computerdb->{$ip}{switch_port_hr},
1396         $computerdb->{$ip}{hostname_fq},
1397         $ip,
1398         $computerdb->{$ip}{mac_address},
1399         $date,
1400         $vlan;
1401      }
1402   return;
1403   }
1404
1405sub cmd_exportdb_html {
1406   test_maindb_environnement();
1407
1408   my $computerdb = computerdb_load();
1409
1410#<link rel="stylesheet" type="text/css" href="style-klask.css" />
1411#<script src="sorttable-klask.js"></script>
1412
1413   print <<'END_HTML';
1414<table class="sortable" summary="Klask Host Database">
1415 <caption>Klask Host Database</caption>
1416 <thead>
1417  <tr>
1418   <th scope="col" class="klask-header-left">Switch</th>
1419   <th scope="col" class="sorttable_nosort">Port</th>
1420   <th scope="col" class="sorttable_nosort" colspan="2">Link</th>
1421   <th scope="col" class="sorttable_alpha">Hostname-FQ</th>
1422   <th scope="col" class="hklask-ipv4">IPv4-Address</th>
1423   <th scope="col" class="sorttable_alpha">MAC-Address</th>
1424   <th scope="col" class="sorttable_alpha">VLAN</th>
1425   <th scope="col" class="klask-header-right">Date</th>
1426  </tr>
1427 </thead>
1428 <tfoot>
1429  <tr>
1430   <th scope="col" class="klask-footer-left">Switch</th>
1431   <th scope="col" class="fklask-port">Port</th>
1432   <th scope="col" class="fklask-link" colspan="2">Link</th>
1433   <th scope="col" class="fklask-hostname">Hostname-FQ</th>
1434   <th scope="col" class="fklask-ipv4">IPv4-Address</th>
1435   <th scope="col" class="fklask-mac">MAC-Address</th>
1436   <th scope="col" class="fklask-vlan">VLAN</th>
1437   <th scope="col" class="klask-footer-right">Date</th>
1438  </tr>
1439 </tfoot>
1440 <tbody>
1441END_HTML
1442
1443   my %mac_count = ();
1444   LOOP_ON_IP_ADDRESS:
1445   for my $ip (keys %{$computerdb}) {
1446
1447      # to be improve in the future
1448      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !
1449
1450      $mac_count{$computerdb->{$ip}{mac_address}}++;
1451      }
1452
1453   my $typerow = 'even';
1454
1455   LOOP_ON_IP_ADDRESS:
1456   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
1457
1458      # to be improve in the future
1459      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !
1460
1461      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
1462      $year += 1900;
1463      $mon++;
1464      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1465
1466#      $odd_or_even++;
1467#      my $typerow = $odd_or_even % 2 ? 'odd' : 'even';
1468      $typerow = $typerow eq 'even' ? 'odd' : 'even';
1469
1470      my $arrow ='&#8592;';
1471         $arrow ='&#8656;' if $computerdb->{$ip}{switch_port_hr} =~ m/^(Trk|Br|Po)/;
1472
1473      my $switch_hostname = $computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description} || 'unkown';
1474      chomp $switch_hostname;
1475      my $switch_hostname_sort = sprintf '%s %3s' ,$switch_hostname, $computerdb->{$ip}{switch_port_hr};
1476
1477      my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ip;
1478
1479      my $mac_sort = sprintf '%04i-%s', 9999 - $mac_count{$computerdb->{$ip}{mac_address}}, $computerdb->{$ip}{mac_address};
1480
1481      $computerdb->{$ip}{hostname_fq} = 'unknow' if $computerdb->{$ip}{hostname_fq} =~ m/^ \d+ \. \d+ \. \d+ \. \d+ $/xms;
1482      my ( $host_short ) = split m/ \. /xms, $computerdb->{$ip}{hostname_fq};
1483
1484      my $vlan = '';
1485      $vlan = $computerdb->{$ip}{network}.' ('.get_current_vlan_id($computerdb->{$ip}{network}).')' if $computerdb->{$ip}{network};
1486
1487      print <<"END_HTML";
1488  <tr class="$typerow">
1489   <td sorttable_customkey="$switch_hostname_sort">$switch_hostname</td>
1490   <td class="bklask-port">$computerdb->{$ip}{switch_port_hr}</td>
1491   <td colspan="2">$arrow</td>
1492   <td sorttable_customkey="$host_short">$computerdb->{$ip}{hostname_fq}</td>
1493   <td sorttable_customkey="$ip_sort">$ip</td>
1494   <td sorttable_customkey="$mac_sort">$computerdb->{$ip}{mac_address}</td>
1495   <td>$vlan</td>
1496   <td>$date</td>
1497  </tr>
1498END_HTML
1499      }
1500
1501   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
1502
1503   my %db_switch_output_port       = %{$switch_connection->{output_port}};
1504   my %db_switch_parent            = %{$switch_connection->{parent}};
1505   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};
1506   my %db_switch                   = %{$switch_connection->{switch_db}};
1507
1508   for my $sw (sort keys %db_switch_output_port) {
1509
1510      my $switch_hostname_sort = sprintf '%s %3s' ,$sw, $db_switch_output_port{$sw};
1511
1512      $typerow = $typerow eq 'even' ? 'odd' : 'even';
1513
1514      my $arrow ='&#8702;';
1515         $arrow ='&#8680;' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
1516
1517      if (exists $db_switch_parent{$sw}) {
1518         my $mac_address = $db_switch{$db_switch_parent{$sw}->{switch}}->{mac_address};
1519         my $ipv4_address = $db_switch{$db_switch_parent{$sw}->{switch}}->{ipv4_address};
1520         my $timestamp = $db_switch{$db_switch_parent{$sw}->{switch}}->{timestamp};
1521
1522         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $timestamp;
1523         $year += 1900;
1524         $mon++;
1525         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1526
1527         my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ [\.\*] /xms, $ipv4_address; # \* for fake-ip
1528
1529         my $mac_sort = sprintf '%04i-%s', 9999, $mac_address;
1530
1531         my ( $host_short ) = sprintf '%s %3s' , split(m/ \. /xms, $db_switch_parent{$sw}->{switch}, 1), $db_switch_parent{$sw}->{port_hr};
1532
1533         print <<"END_HTML";
1534  <tr class="$typerow">
1535   <td sorttable_customkey="$switch_hostname_sort">$sw</td>
1536   <td class="bklask-port">$db_switch_output_port{$sw}</td>
1537   <td>$arrow</td><td>$db_switch_parent{$sw}->{port_hr}</td>
1538   <td sorttable_customkey="$host_short">$db_switch_parent{$sw}->{switch}</td>
1539   <td sorttable_customkey="$ip_sort">$ipv4_address</td>
1540   <td sorttable_customkey="$mac_sort">$mac_address</td>
1541   <td></td>
1542   <td>$date</td>
1543  </tr>
1544END_HTML
1545         }
1546      else {
1547         print <<"END_HTML";
1548  <tr class="$typerow">
1549   <td sorttable_customkey="$switch_hostname_sort">$sw</td>
1550   <td class="bklask-port">$db_switch_output_port{$sw}</td>
1551   <td>$arrow</td><td></td>
1552   <td sorttable_customkey="router">router</td>
1553   <td sorttable_customkey="999999999999"></td>
1554   <td sorttable_customkey="99999"></td>
1555   <td></td>
1556   <td></td>
1557  </tr>
1558END_HTML
1559         }
1560      }
1561
1562   for my $swport (sort keys %db_switch_connected_on_port) {
1563      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
1564      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
1565
1566         my $switch_hostname_sort = sprintf '%s %3s' ,$sw_connect, $port_connect;
1567
1568         my $mac_address = $db_switch{$sw}->{mac_address};
1569         my $ipv4_address = $db_switch{$sw}->{ipv4_address};
1570         my $timestamp = $db_switch{$sw}->{timestamp};
1571
1572         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $timestamp;
1573         $year += 1900;
1574         $mon++;
1575         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year,$mon,$mday,$hour,$min;
1576
1577         my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ [\.\*] /xms, $ipv4_address; # \* for fake-ip
1578
1579         my $mac_sort = sprintf '%04i-%s', 9999, $mac_address;
1580
1581         $typerow = $typerow eq 'even' ? 'odd' : 'even';
1582
1583         my $arrow ='&#8701;';
1584            $arrow ='&#8678;' if $port_connect =~ m/^(Trk|Br|Po)/;
1585
1586         if (exists $db_switch_output_port{$sw}) {
1587
1588            my ( $host_short ) = sprintf '%s %3s' , split( m/\./xms, $sw, 1), $db_switch_output_port{$sw};
1589
1590            print <<"END_HTML";
1591  <tr class="$typerow">
1592   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
1593   <td class="bklask-port">$port_connect</td>
1594   <td>$arrow</td><td>$db_switch_output_port{$sw}</td>
1595   <td sorttable_customkey="$host_short">$sw</td>
1596   <td sorttable_customkey="$ip_sort">$ipv4_address</td>
1597   <td sorttable_customkey="$mac_sort">$mac_address</td>
1598   <td></td>
1599   <td>$date</td>
1600  </tr>
1601END_HTML
1602            }
1603         else {
1604            print <<"END_HTML";
1605  <tr class="$typerow">
1606   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
1607   <td class="bklask-port">$port_connect</td>
1608   <td>$arrow</td><td></td>
1609   <td sorttable_customkey="$sw">$sw</td>
1610   <td sorttable_customkey="">$ipv4_address</td>
1611   <td sorttable_customkey="">$mac_address</td>
1612   <td></td>
1613   <td>$date</td>
1614  </tr>
1615END_HTML
1616            }
1617         }
1618      }
1619
1620   print <<'END_HTML';
1621 </tbody>
1622</table>
1623END_HTML
1624   return;
1625   }
1626
1627sub cmd_bad_vlan_id {
1628   test_maindb_environnement();
1629
1630   my $computerdb = computerdb_load();
1631
1632   # create a database with the most recent computer by switch port
1633   my %switchportdb = ();
1634   LOOP_ON_IP_ADDRESS:
1635   for my $ip (keys %{$computerdb}) {
1636      # to be improve in the future
1637      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !
1638      next LOOP_ON_IP_ADDRESS if ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}) eq 'unknow';
1639      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{switch_port} eq '0';
1640
1641      my $ip_timestamp   = $computerdb->{$ip}{timestamp};
1642      my $ip_mac         = $computerdb->{$ip}{mac_address};
1643      my $ip_hostname_fq = $computerdb->{$ip}{hostname_fq};
1644
1645      my $swpt = sprintf "%-28s  %2s",
1646         $computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description},
1647         $computerdb->{$ip}{switch_port_hr};
1648      $switchportdb{$swpt} ||= {
1649         ip          => $ip,
1650         timestamp   => $ip_timestamp,
1651         vlan        => $computerdb->{$ip}{network},
1652         hostname_fq => $ip_hostname_fq,
1653         mac_address => $ip_mac,
1654         };
1655
1656      # if float computer, set date 15 day before warning...
1657      my $ip_timestamp_mod = $ip_timestamp;
1658      my $ip_timestamp_ref = $switchportdb{$swpt}->{timestamp};
1659      $ip_timestamp_mod -= 15 * 24 * 3600 if $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;
1660      $ip_timestamp_ref -= 15 * 24 * 3600 if $switchportdb{$swpt}->{hostname_fq} =~ m/$RE_FLOAT_HOSTNAME/;
1661
1662      if ($ip_timestamp_mod > $ip_timestamp_ref) {
1663         $switchportdb{$swpt} = {
1664            ip          => $ip,
1665            timestamp   => $ip_timestamp,
1666            vlan        => $computerdb->{$ip}{network},
1667            hostname_fq => $ip_hostname_fq,
1668            mac_address => $ip_mac,
1669            };
1670         }
1671      }
1672
1673   LOOP_ON_RECENT_COMPUTER:
1674   for my $swpt (keys %switchportdb) {
1675      next LOOP_ON_RECENT_COMPUTER if $swpt =~ m/^\s*0$/;
1676      next LOOP_ON_RECENT_COMPUTER if $switchportdb{$swpt}->{hostname_fq} !~ m/$RE_FLOAT_HOSTNAME/;
1677
1678      my $src_ip = $switchportdb{$swpt}->{ip};
1679      my $src_timestamp = 0;
1680      LOOP_ON_IP_ADDRESS:
1681      for my $ip (keys %{$computerdb}) {
1682         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{mac_address} ne  $switchportdb{$swpt}->{mac_address};
1683         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} =~ m/$RE_FLOAT_HOSTNAME/;
1684         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{timestamp} < $src_timestamp;
1685
1686         $src_ip = $ip;
1687         $src_timestamp = $computerdb->{$ip}{timestamp};
1688         }
1689
1690      # keep only if float computer is the most recent
1691      next LOOP_ON_RECENT_COMPUTER if $src_timestamp == 0;
1692      next LOOP_ON_RECENT_COMPUTER if $switchportdb{$swpt}->{timestamp} < $src_timestamp;
1693
1694      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $switchportdb{$swpt}->{timestamp};
1695      $year += 1900;
1696      $mon++;
1697      my $date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;
1698
1699      ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$src_ip}{timestamp};
1700      $year += 1900;
1701      $mon++;
1702      my $src_date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;
1703
1704      my $vlan_id = get_current_vlan_id($computerdb->{$src_ip}{network});
1705
1706      printf "%s / %-10s +-> %-10s(%i)  %s %s %s %s\n",
1707         $swpt, $switchportdb{$swpt}->{vlan}, $computerdb->{$src_ip}{network}, $vlan_id,
1708         $date,
1709         $src_date,
1710         $computerdb->{$src_ip}{mac_address},
1711         $computerdb->{$src_ip}{hostname_fq};
1712      }
1713   }
1714
1715sub cmd_set_vlan_port {
1716   my $switch_name = shift || q{};
1717   my $mac_address = shift || q{};
1718
1719   if ($switch_name eq q{} or $mac_address eq q{}) {
1720      die "Usage: klask search-mac-on-switch SWITCH_NAME MAC_ADDRESS\n";
1721      }
1722
1723   $switch_name = join(',', map {$_->{hostname}} @SWITCH_LIST ) if $switch_name eq q{*};
1724
1725   for my $sw_name (split /,/, $switch_name) {
1726      if (not defined $SWITCH_DB{$sw_name}) {
1727         die "Switch $sw_name must be defined in klask configuration file\n";
1728         }
1729
1730      my $research1 = $OID_NUMBER{searchPort1} . mac_address_hex_to_dec($mac_address);
1731      my $research2 = $OID_NUMBER{searchPort2} .'.'. 0 . mac_address_hex_to_dec($mac_address);
1732      print "Klask search OID $research1 on switch $sw_name\n";
1733      print "Klask search OID $research2 on switch $sw_name\n";
1734
1735      my $sw = $SWITCH_DB{$sw_name};
1736      my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
1737      print "$error \n" if $error;
1738
1739      my $result = $session->get_request(
1740         -varbindlist => [$research1]
1741         );
1742      if (not defined $result) {
1743         $result = $session->get_request(
1744            -varbindlist => [$research2]
1745            );
1746         $result->{$research1} = $result->{$research2} if defined $result;
1747         }
1748
1749      if (defined $result and $result->{$research1} ne 'noSuchInstance') {
1750         my $swport = $result->{$research1};
1751         print "Klask find MAC $mac_address on switch $sw_name port $swport\n";
1752         }
1753      else {
1754         print "Klask do not find MAC $mac_address on switch $sw_name\n";
1755         }
1756
1757      $session->close;
1758      }
1759   return;
1760   }
1761
1762sub cmd_get_vlan_port {
1763   @ARGV = @_;
1764
1765   my $verbose;
1766   GetOptions(
1767      'verbose|v' => \$verbose,
1768      );
1769
1770   my $switch_name = shift @ARGV || q{};
1771   my $switch_port = shift @ARGV || q{};
1772
1773   if ($switch_name eq q{} or $switch_port eq q{}) {
1774      die "Usage: klask get-vlan-port SWITCH_NAME PORT\n";
1775      }
1776
1777   for my $sw_name (split /,/, $switch_name) {
1778      if (not defined $SWITCH_DB{$sw_name}) {
1779         die "Switch $sw_name must be defined in klask configuration file\n";
1780         }
1781
1782      my $search = $OID_NUMBER{'vlanPortDefault'} . ".$switch_port";
1783
1784      my $sw = $SWITCH_DB{$sw_name};
1785      my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
1786      print "$error \n" if $error;
1787
1788      my $result = $session->get_request(
1789         -varbindlist => [$search],
1790         );
1791
1792      if (defined $result and $result->{$search} ne 'noSuchInstance') {
1793         my $vlan_id = $result->{$search} || 'empty';
1794         print "Klask VLAN Id $vlan_id on switch $sw_name on port $switch_port\n";
1795         }
1796      else {
1797         print "Klask do not find VLAN Id on switch $sw_name on port $switch_port\n";
1798         }
1799
1800      $session->close;
1801      }
1802   return;
1803   }
1804
1805sub cmd_set_vlan_name {
1806   }
1807
1808# snmpset -v 1 -c public sw1-batG0-legi.hmg.priv "$OID_NUMBER{'hpicfReset'}.0" i 2;
1809sub cmd_rebootsw {
1810   @ARGV = @_;
1811
1812   my $verbose;
1813   GetOptions(
1814      'verbose|v' => \$verbose,
1815      );
1816
1817   my $switch_name = shift @ARGV || q{};
1818
1819   if ($switch_name eq q{}) {
1820      die "Usage: klask rebootsw SWITCH_NAME\n";
1821      }
1822
1823   for my $sw_name (split /,/, $switch_name) {
1824      if (not defined $SWITCH_DB{$sw_name}) {
1825         die "Switch $sw_name must be defined in klask configuration file\n";
1826         }
1827
1828      my $sw = $SWITCH_DB{$sw_name};
1829      my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
1830      print "$error \n" if $error;
1831
1832      my $result = $session->set_request(
1833         -varbindlist => ["$OID_NUMBER{'hpicfReset'}.0", INTEGER, 2],
1834         );
1835
1836      $session->close;
1837      }
1838   return;
1839   }
1840
1841sub cmd_get_vlan_name {
1842   my $switch_name = shift || q{};
1843   my $vlan_id     = shift || q{};
1844
1845   if ($switch_name eq q{} or $vlan_id eq q{}) {
1846      die "Usage: klask get-vlan-name SWITCH_NAME VLAN_ID\n";
1847      }
1848
1849   $switch_name = join(',', map {$_->{hostname}} @SWITCH_LIST ) if $switch_name eq q{*};
1850
1851   for my $sw_name (split /,/, $switch_name) {
1852      if (not defined $SWITCH_DB{$sw_name}) {
1853         die "Switch $sw_name must be defined in klask configuration file\n";
1854         }
1855
1856      my $search_vlan_name = $OID_NUMBER{vlanName} . ".$vlan_id";
1857
1858      my $sw = $SWITCH_DB{$sw_name};
1859      my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
1860      print "$error \n" if $error;
1861
1862      my $result = $session->get_request(
1863         -varbindlist => [$search_vlan_name]
1864         );
1865
1866      if (defined $result and $result->{$search_vlan_name} ne 'noSuchInstance') {
1867         my $vlan_name = $result->{$search_vlan_name} || 'empty';
1868         print "Klask find VLAN $vlan_id on switch $sw_name with name $vlan_name\n";
1869         }
1870      else {
1871         print "Klask do not find VLAN $vlan_id on switch $sw_name\n";
1872         }
1873
1874      $session->close;
1875      }
1876   return;
1877   }
1878
1879sub cmd_ip_location {
1880   my $computerdb = computerdb_load();
1881
1882   LOOP_ON_IP_ADDRESS:
1883   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
1884
1885      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !
1886
1887      my $sw_hostname = $computerdb->{$ip}{switch_hostname} || q{};
1888      next LOOP_ON_IP_ADDRESS if $sw_hostname eq 'unknow';
1889
1890      my $sw_location = q{};
1891      LOOP_ON_ALL_SWITCH:
1892      for my $sw (@SWITCH_LIST) {
1893         next LOOP_ON_ALL_SWITCH if $sw_hostname ne $sw->{hostname};
1894         $sw_location = $sw->{location};
1895         last;
1896         }
1897
1898      printf "%s: \"%s\"\n", $ip, $sw_location if not $sw_location eq q{};
1899      }
1900   return;
1901   }
1902
1903sub cmd_ip_free {
1904   @ARGV = @_; # VLAN name with option
1905
1906   my $days_to_dead = 365 * 2;
1907   my $format = 'txt';
1908   my $verbose;
1909
1910   GetOptions(
1911      'day|d=i'      => \$days_to_dead,
1912      'format|f=s'   => \$format,
1913      'verbose|v'    => \$verbose,
1914      );
1915
1916   my %possible_format = (
1917      txt  => \&cmd_ip_free_txt,
1918      html => \&cmd_ip_free_html,
1919      none => sub {},
1920      );
1921   $format = 'txt' if not defined $possible_format{$format};
1922
1923   my @vlan_name = @ARGV;
1924   @vlan_name = get_list_network() if not @vlan_name;
1925
1926   my $computerdb = {};
1927      $computerdb = computerdb_load() if -e "$KLASK_DB_FILE";
1928   my $timestamp = time;
1929
1930   my $timestamp_barrier = $timestamp - (3600 * 24 * $days_to_dead );
1931
1932   my %result_ip = ();
1933
1934   ALL_NETWORK:
1935   for my $vlan (@vlan_name) {
1936
1937      my @ip_list = get_list_ip($vlan);
1938
1939      LOOP_ON_IP_ADDRESS:
1940      for my $ip (@ip_list) {
1941
1942         if (exists $computerdb->{$ip}) {
1943            next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{timestamp} > $timestamp_barrier;
1944
1945            my $mac_address = $computerdb->{$ip}{mac_address};
1946            LOOP_ON_DATABASE:
1947            for my $ip_db (keys %{$computerdb}) {
1948               next LOOP_ON_DATABASE if $computerdb->{$ip_db}{mac_address} ne $mac_address;
1949               next LOOP_ON_IP_ADDRESS if $computerdb->{$ip_db}{timestamp} > $timestamp_barrier;
1950               }
1951            }
1952
1953         my $ip_date_last_detection = '';
1954         if (exists $computerdb->{$ip}) {
1955            my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
1956            $year += 1900;
1957            $mon++;
1958            $ip_date_last_detection = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1959            }
1960
1961         my $packed_ip = scalar gethostbyname($ip);
1962         my $hostname_fq = 'unknown';
1963            $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET) || 'unknown' if defined $packed_ip and get_current_scan_mode($vlan) eq 'active';
1964
1965         next LOOP_ON_IP_ADDRESS if $hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;
1966
1967         $result_ip{$ip} ||= {};
1968         $result_ip{$ip}->{date_last_detection} = $ip_date_last_detection;
1969         $result_ip{$ip}->{hostname_fq} = $hostname_fq;
1970         $result_ip{$ip}->{vlan} = $vlan;
1971
1972         printf "VERBOSE_1: %-15s %-12s %s\n", $ip, $vlan, $hostname_fq if $verbose;
1973         }
1974      }
1975
1976   $possible_format{$format}->(%result_ip);
1977   }
1978
1979sub cmd_ip_free_txt {
1980   my %result_ip = @_;
1981
1982   printf "%-15s %-40s %-16s %s\n", qw(IPv4-Address Hostname-FQ Date VLAN);
1983   print "-------------------------------------------------------------------------------\n";
1984   LOOP_ON_IP_ADDRESS:
1985   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {
1986         my $vlan_nameid = $result_ip{$ip}->{vlan}.'('.get_current_vlan_id($result_ip{$ip}->{vlan}).')';
1987         printf "%-15s %-40s %-16s %s\n", $ip, $result_ip{$ip}->{hostname_fq}, $result_ip{$ip}->{date_last_detection}, $vlan_nameid;
1988      }
1989   }
1990
1991sub cmd_ip_free_html {
1992   my %result_ip = @_;
1993
1994   print <<'END_HTML';
1995<table class="sortable" summary="Klask Free IP Database">
1996 <caption>Klask Free IP Database</caption>
1997 <thead>
1998  <tr>
1999   <th scope="col" class="klask-header-left">IPv4-Address</th>
2000   <th scope="col" class="sorttable_alpha">Hostname-FQ</th>
2001   <th scope="col" class="sorttable_alpha">VLAN</th>
2002   <th scope="col" class="klask-header-right">Date</th>
2003  </tr>
2004 </thead>
2005 <tfoot>
2006  <tr>
2007   <th scope="col" class="klask-footer-left">IPv4-Address</th>
2008   <th scope="col" class="fklask-hostname">Hostname-FQ</th>
2009   <th scope="col" class="fklask-vlan">VLAN</th>
2010   <th scope="col" class="klask-footer-right">Date</th>
2011  </tr>
2012 </tfoot>
2013 <tbody>
2014END_HTML
2015
2016   my $typerow = 'even';
2017
2018   LOOP_ON_IP_ADDRESS:
2019   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {
2020
2021      $typerow = $typerow eq 'even' ? 'odd' : 'even';
2022
2023      my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ip;
2024      my ( $host_short ) = split m/ \. /xms, $result_ip{$ip}->{hostname_fq};
2025
2026      my $vlan_nameid = $result_ip{$ip}->{vlan}.'('.get_current_vlan_id($result_ip{$ip}->{vlan}).')';
2027
2028      print <<"END_HTML";
2029  <tr class="$typerow">
2030   <td sorttable_customkey="$ip_sort">$ip</td>
2031   <td sorttable_customkey="$host_short">$result_ip{$ip}->{hostname_fq}</td>
2032   <td>$vlan_nameid</td>
2033   <td>$result_ip{$ip}->{date_last_detection}</td>
2034  </tr>
2035END_HTML
2036      }
2037   print <<'END_HTML';
2038 </tbody>
2039</table>
2040END_HTML
2041   }
2042
2043sub cmd_enable {
2044   @ARGV = @_;
2045
2046   my $verbose;
2047
2048   GetOptions(
2049      'verbose|v' => \$verbose,
2050      );
2051
2052   my $switch_name = shift @ARGV || q{};
2053   my $port        = shift @ARGV || q{};
2054
2055   if ($switch_name eq q{} or $port eq q{}) {
2056      die "Usage: klask disable SWITCH_NAME PORT\n";
2057      }
2058
2059   if (not defined $SWITCH_DB{$switch_name}) {
2060      die "Switch $switch_name must be defined in klask configuration file\n";
2061      }
2062
2063   my $search_portstatus = $OID_NUMBER{'portUpDown'} .'.'. $port;
2064   print "Info: switch $switch_name port $port SNMP OID $search_portstatus\n" if $verbose;
2065
2066   my $sw = $SWITCH_DB{$switch_name};
2067   my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
2068   print "$error \n" if $error;
2069
2070   my $result = $session->set_request(
2071      -varbindlist => [$search_portstatus, INTEGER, 1],
2072      );
2073
2074   $session->close;
2075
2076   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 1 (up)
2077   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 2 (down)
2078   #system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 1";
2079   return;
2080   }
2081
2082sub cmd_disable {
2083   @ARGV = @_;
2084
2085   my $verbose;
2086
2087   GetOptions(
2088      'verbose|v' => \$verbose,
2089      );
2090
2091   my $switch_name = shift @ARGV || q{};
2092   my $port        = shift @ARGV || q{};
2093
2094   if ($switch_name eq q{} or $port eq q{}) {
2095      die "Usage: klask disable SWITCH_NAME PORT\n";
2096      }
2097
2098   if (not defined $SWITCH_DB{$switch_name}) {
2099      die "Switch $switch_name must be defined in klask configuration file\n";
2100      }
2101
2102   my $search_portstatus = $OID_NUMBER{'portUpDown'} .'.'. $port;
2103   print "Info: switch $switch_name port $port SNMP OID $search_portstatus\n" if $verbose;
2104
2105   my $sw = $SWITCH_DB{$switch_name};
2106   my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
2107   print "$error \n" if $error;
2108
2109   my $result = $session->set_request(
2110      -varbindlist => [$search_portstatus, INTEGER, 2],
2111      );
2112   print $session->error()."\n" if $session->error_status();
2113
2114   $session->close;
2115
2116   #system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 2";
2117   return;
2118   }
2119
2120sub cmd_status {
2121   @ARGV = @_;
2122
2123   my $verbose;
2124
2125   GetOptions(
2126      'verbose|v' => \$verbose,
2127      );
2128
2129   my $switch_name = shift @ARGV || q{};
2130   my $port        = shift @ARGV || q{};
2131
2132   if ($switch_name eq q{} or $port eq q{}) {
2133      die "Usage: klask status SWITCH_NAME PORT\n";
2134      }
2135
2136   if (not defined $SWITCH_DB{$switch_name}) {
2137      die "Switch $switch_name must be defined in klask configuration file\n";
2138      }
2139
2140   my $sw = $SWITCH_DB{$switch_name};
2141   my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
2142   print "$error \n" if $error;
2143
2144   # Retrieve numeric port value
2145   my $port_num = snmp_get_switchport_num($session, normalize_port_human_readable($port), $verbose ? 'yes' : '');
2146   die "Error : Port $port not exist on switch $switch_name\n" if not $port_num =~ m/^\d+$/;
2147
2148   my $search_portstatus = $OID_NUMBER{'portUpDown'} .'.'. $port_num;
2149   print "Info: switch $switch_name port $port ($port_num) SNMP OID $search_portstatus\n" if $verbose;
2150
2151   my $result = $session->get_request(
2152      -varbindlist => [$search_portstatus]
2153      );
2154   if (defined $result) {
2155      print "$PORT_UPDOWN{$result->{$search_portstatus}}\n";
2156      }
2157
2158   #system "snmpget -v 1 -c public $switch_name 1.3.6.1.2.1.2.2.1.7.$port";
2159   return;
2160   }
2161
2162sub cmd_search_mac_on_switch {
2163   @ARGV = @_;
2164
2165   my $verbose;
2166   my $vlan_id = 0;
2167
2168   GetOptions(
2169      'verbose|v' => \$verbose,
2170      'vlan|l=i'  => \$vlan_id,
2171      );
2172
2173   my $switch_name = shift @ARGV || q{};
2174   my $mac_address = shift @ARGV || q{};
2175
2176   if ($switch_name eq q{} or $mac_address eq q{}) {
2177      die "Usage: klask search-mac-on-switch SWITCH_NAME MAC_ADDRESS\n";
2178      }
2179
2180   $mac_address = normalize_mac_address($mac_address);
2181   $switch_name = join(',', map {$_->{hostname}} @SWITCH_LIST ) if $switch_name eq q{*};
2182
2183   for my $sw_name (split /,/, $switch_name) {
2184      if (not defined $SWITCH_DB{$sw_name}) {
2185         die "Switch $sw_name must be defined in klask configuration file\n";
2186         }
2187
2188      my $research1 = $OID_NUMBER{searchPort1} . mac_address_hex_to_dec($mac_address);
2189      my $research2 = $OID_NUMBER{searchPort2} .'.'. $vlan_id . mac_address_hex_to_dec($mac_address);
2190      print "Klask search OID $research1 on switch $sw_name\n" if $verbose;
2191      print "Klask search OID $research2 on switch $sw_name\n" if $verbose;
2192
2193      my $sw = $SWITCH_DB{$sw_name};
2194      my ($session, $error) = Net::SNMP->session( %{$sw->{snmp_param_session}} );
2195      print "$error \n" if $error;
2196
2197      my $result = $session->get_request(
2198         -varbindlist => [$research1]
2199         );
2200      if (not defined $result) {
2201         $result = $session->get_request(
2202            -varbindlist => [$research2]
2203            );
2204         $result->{$research1} = $result->{$research2} if defined $result;
2205         }
2206
2207      if (defined $result and $result->{$research1} ne 'noSuchInstance') {
2208         my $swport_num = $result->{$research1};
2209         my $swport_hr = get_human_readable_port($sw->{model}, snmp_get_switchport_hr($session, $swport_num));
2210         print "Klask find MAC $mac_address on switch $sw_name port $swport_hr\n";
2211         }
2212      else {
2213         print "Klask do not find MAC $mac_address on switch $sw_name\n" if $verbose;
2214         }
2215
2216      $session->close;
2217      }
2218   return;
2219   }
2220
2221sub cmd_updatesw {
2222   @ARGV = @_;
2223
2224   my $verbose;
2225
2226   GetOptions(
2227      'verbose|v' => \$verbose,
2228      );
2229
2230   init_switch_names('yes');    #nomme les switchs
2231   print "\n";
2232
2233   my %where = ();
2234   my %db_switch_output_port = ();
2235   my %db_switch_ip_hostnamefq = ();
2236
2237   DETECT_ALL_ROUTER:
2238   for my $one_router ( get_list_main_router(get_list_network()) ) {
2239      print "Info: router loop $one_router\n" if $verbose;
2240      my %resol_arp = resolve_ip_arp_host($one_router, q{*}, q{low}); # resolution arp
2241
2242      next DETECT_ALL_ROUTER if $resol_arp{mac_address} eq 'unknow';
2243      print "VERBOSE_1: Router detected $resol_arp{ipv4_address} - $resol_arp{mac_address}\n" if $verbose;
2244
2245      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{interface});
2246      my $vlan_id   = get_current_vlan_id($vlan_name);
2247      $where{$resol_arp{ipv4_address}} = find_all_switch_port($resol_arp{mac_address},$vlan_id); # retrouve les emplacements des routeurs
2248      }
2249
2250   ALL_ROUTER_IP_ADDRESS:
2251   for my $ip_router (Net::Netmask::sort_by_ip_address(keys %where)) { # '194.254.66.254')) {
2252
2253      next ALL_ROUTER_IP_ADDRESS if not exists $where{$ip_router}; # /a priori/ idiot car ne sers à rien...
2254
2255      ALL_SWITCH_CONNECTED:
2256      for my $switch_detected ( keys %{$where{$ip_router}} ) {
2257
2258         my $switch = $where{$ip_router}->{$switch_detected};
2259
2260         next ALL_SWITCH_CONNECTED if $switch->{port} eq '0';
2261
2262         $db_switch_output_port{$switch->{hostname}} = $switch->{port_hr};
2263         print "VERBOSE_2: output port $switch->{hostname} : $switch->{port_hr}\n" if $verbose;
2264         }
2265      }
2266
2267   my %db_switch_link_with = ();
2268
2269   my @list_all_switch = ();
2270   my @list_switch_ipv4 = ();
2271   for my $sw (@SWITCH_LIST) {
2272      push @list_all_switch, $sw->{hostname};
2273      }
2274
2275   my $timestamp = time;
2276
2277   ALL_SWITCH:
2278   for my $one_switch (@list_all_switch) {
2279      my %resol_arp = resolve_ip_arp_host($one_switch, q{*}, q{low}); # arp resolution
2280      if (exists $SWITCH_DB{$one_switch}{'fake-ip'}) {
2281         print "WARNING: fake ip on switch $one_switch -> $SWITCH_DB{$one_switch}{'fake-ip'} / $resol_arp{ipv4_address}\n" if $verbose;
2282         my %resol_arp_alt = resolve_ip_arp_host($SWITCH_DB{$one_switch}{'fake-ip'}, q{*}, q{low}); # arp resolution
2283         if ($resol_arp_alt{mac_address} ne 'unknow') {
2284            $resol_arp{mac_address}   = $resol_arp_alt{mac_address};
2285            $resol_arp{interface}     = $resol_arp_alt{interface};
2286            $resol_arp{ipv4_address} .= '*';
2287            }
2288         }
2289      print "Info: switch loop $one_switch\n" if $verbose;
2290      next ALL_SWITCH if $resol_arp{mac_address} eq 'unknow';
2291
2292      push @list_switch_ipv4, $resol_arp{ipv4_address};
2293
2294      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{interface});
2295      my $vlan_id   = get_current_vlan_id($vlan_name);
2296      $where{$resol_arp{ipv4_address}} = find_all_switch_port($resol_arp{mac_address},$vlan_id); # find port on all switch
2297
2298      if ($verbose) {
2299         print "VERBOSE_3: $one_switch $resol_arp{ipv4_address} $resol_arp{mac_address}\n";
2300         print "VERBOSE_3: $one_switch --- ",
2301            join(' + ', keys %{$where{$resol_arp{ipv4_address}}}),
2302            "\n";
2303         }
2304
2305      $db_switch_ip_hostnamefq{$resol_arp{ipv4_address}} = $resol_arp{hostname_fq};
2306      print "VERBOSE_4: db_switch_ip_hostnamefq $resol_arp{ipv4_address} -> $resol_arp{hostname_fq}\n" if $verbose;
2307
2308      $SWITCH_DB{$one_switch}->{ipv4_address} = $resol_arp{ipv4_address};
2309      $SWITCH_DB{$one_switch}->{mac_address}  = $resol_arp{mac_address};
2310      $SWITCH_DB{$one_switch}->{timestamp}    = $timestamp;
2311      }
2312
2313   ALL_SWITCH_IP_ADDRESS:
2314   for my $ip (@list_switch_ipv4) {
2315#   for my $ip (Net::Netmask::sort_by_ip_address(@list_switch_ipv4)) {
2316
2317      print "VERBOSE_5: loop on $db_switch_ip_hostnamefq{$ip}\n" if $verbose;
2318
2319      next ALL_SWITCH_IP_ADDRESS if not exists $where{$ip};
2320#      next ALL_SWITCH_IP_ADDRESS if not exists $SWITCH_PORT_COUNT{ $db_switch_ip_hostnamefq{$ip} };
2321
2322      DETECTED_SWITCH:
2323      for my $switch_detected ( keys %{$where{$ip}} ) {
2324
2325         my $switch = $where{$ip}->{$switch_detected};
2326         print "VERBOSE_6: $db_switch_ip_hostnamefq{$ip} -> $switch->{hostname} : $switch->{port_hr}\n" if $verbose;
2327
2328         next if $switch->{port}     eq '0';
2329         next if $switch->{port_hr}  eq $db_switch_output_port{$switch->{hostname}};
2330         next if $switch->{hostname} eq $db_switch_ip_hostnamefq{$ip}; # $computerdb->{$ip}{hostname};
2331
2332         $db_switch_link_with{ $db_switch_ip_hostnamefq{$ip} } ||= {};
2333         $db_switch_link_with{ $db_switch_ip_hostnamefq{$ip} }->{ $switch->{hostname} } = $switch->{port_hr};
2334         print "VERBOSE_7: +++++\n" if $verbose;
2335         }
2336
2337      }
2338
2339   my %db_switch_connected_on_port = ();
2340   my $maybe_more_than_one_switch_connected = 'yes';
2341   my $cloop = 0;
2342
2343   while ($maybe_more_than_one_switch_connected eq 'yes' and $cloop < 100) {
2344      $cloop++;
2345      print "VERBOSE_9: cloop reduction step: $cloop\n" if $verbose;
2346      for my $sw (keys %db_switch_link_with) {
2347         for my $connect (keys %{$db_switch_link_with{$sw}}) {
2348
2349            my $port_hr = $db_switch_link_with{$sw}->{$connect};
2350
2351            $db_switch_connected_on_port{"$connect:$port_hr"} ||= {};
2352            $db_switch_connected_on_port{"$connect:$port_hr"}->{$sw}++; # Just to define the key
2353            }
2354         }
2355
2356      $maybe_more_than_one_switch_connected  = 'no';
2357
2358      SWITCH_AND_PORT:
2359      for my $swport (keys %db_switch_connected_on_port) {
2360
2361         next if keys %{$db_switch_connected_on_port{$swport}} == 1;
2362
2363         $maybe_more_than_one_switch_connected = 'yes';
2364
2365         my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
2366         my @sw_on_same_port = keys %{$db_switch_connected_on_port{$swport}};
2367         print "VERBOSE_10: $swport -- ".$#sw_on_same_port." -- @sw_on_same_port\n" if $verbose;
2368
2369         CONNECTED:
2370         for my $sw_connected (@sw_on_same_port) {
2371
2372            next CONNECTED if not keys %{$db_switch_link_with{$sw_connected}} == 1;
2373
2374            $db_switch_connected_on_port{$swport} = {$sw_connected => 1};
2375
2376            for my $other_sw (@sw_on_same_port) {
2377               next if $other_sw eq $sw_connected;
2378
2379               delete $db_switch_link_with{$other_sw}->{$sw_connect};
2380               }
2381
2382            # We can not do better for this switch for this loop
2383            next SWITCH_AND_PORT;
2384            }
2385         }
2386      }
2387
2388   my %db_switch_parent =();
2389
2390   for my $sw (keys %db_switch_link_with) {
2391      for my $connect (keys %{$db_switch_link_with{$sw}}) {
2392
2393         my $port_hr = $db_switch_link_with{$sw}->{$connect};
2394
2395         $db_switch_connected_on_port{"$connect:$port_hr"} ||= {};
2396         $db_switch_connected_on_port{"$connect:$port_hr"}->{$sw} = $port_hr;
2397
2398         $db_switch_parent{$sw} = {switch => $connect, port_hr => $port_hr};
2399         }
2400      }
2401
2402   print "Switch output port and parent port connection\n";
2403   print "---------------------------------------------\n";
2404   for my $sw (sort keys %db_switch_output_port) {
2405      if (exists $db_switch_parent{$sw}) {
2406         printf "%-28s  %2s  +-->  %2s  %-25s\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{port_hr}, $db_switch_parent{$sw}->{switch};
2407         }
2408      else {
2409         printf "%-28s  %2s  +-->  router\n", $sw, $db_switch_output_port{$sw};
2410         }
2411      }
2412   print "\n";
2413
2414   print "Switch parent and children port inter-connection\n";
2415   print "------------------------------------------------\n";
2416   for my $swport (sort keys %db_switch_connected_on_port) {
2417      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
2418      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
2419         if (exists $db_switch_output_port{$sw}) {
2420            printf "%-28s  %2s  <--+  %2s  %-25s\n", $sw_connect, $port_connect, $db_switch_output_port{$sw}, $sw;
2421            }
2422         else {
2423            printf "%-28s  %2s  <--+      %-25s\n", $sw_connect, $port_connect, $sw;
2424            }
2425         }
2426      }
2427
2428   my $switch_connection = {
2429      output_port       => \%db_switch_output_port,
2430      parent            => \%db_switch_parent,
2431      connected_on_port => \%db_switch_connected_on_port,
2432      link_with         => \%db_switch_link_with,
2433      switch_db         => \%SWITCH_DB,
2434      };
2435
2436   YAML::Syck::DumpFile("$KLASK_SW_FILE", $switch_connection);
2437   return;
2438   }
2439
2440sub cmd_exportsw {
2441   @ARGV = @_;
2442
2443   test_switchdb_environnement();
2444
2445   my $format = 'txt';
2446
2447   GetOptions(
2448      'format|f=s'  => \$format,
2449      );
2450
2451   my %possible_format = (
2452      txt => \&cmd_exportsw_txt,
2453      dot => \&cmd_exportsw_dot,
2454      );
2455
2456   $format = 'txt' if not defined $possible_format{$format};
2457
2458   $possible_format{$format}->(@ARGV);
2459   return;
2460   }
2461
2462sub cmd_exportsw_txt {
2463
2464   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
2465
2466   my %db_switch_output_port       = %{$switch_connection->{output_port}};
2467   my %db_switch_parent            = %{$switch_connection->{parent}};
2468   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};
2469
2470   print "Switch output port and parent port connection\n";
2471   print "---------------------------------------------\n";
2472   for my $sw (sort keys %db_switch_output_port) {
2473      if (exists $db_switch_parent{$sw}) {
2474         printf "%-28s  %2s  +-->  %2s  %-25s\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{port_hr}, $db_switch_parent{$sw}->{switch};
2475         }
2476      else {
2477         printf "%-28s  %2s  +-->  router\n", $sw, $db_switch_output_port{$sw};
2478         }
2479      }
2480   print "\n";
2481
2482   print "Switch parent and children port inter-connection\n";
2483   print "------------------------------------------------\n";
2484   for my $swport (sort keys %db_switch_connected_on_port) {
2485      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
2486      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
2487         if (exists $db_switch_output_port{$sw}) {
2488            printf "%-28s  %2s  <--+  %2s  %-25s\n", $sw_connect, $port_connect, $db_switch_output_port{$sw}, $sw;
2489            }
2490         else {
2491            printf "%-28s  %2s  <--+      %-25s\n", $sw_connect, $port_connect, $sw;
2492            }
2493         }
2494      }
2495   return;
2496   }
2497
2498sub cmd_exportsw_dot {
2499
2500   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
2501
2502   my %db_switch_output_port       = %{$switch_connection->{output_port}};
2503   my %db_switch_parent            = %{$switch_connection->{parent}};
2504   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};
2505   my %db_switch_link_with         = %{$switch_connection->{link_with}};
2506   my %db_switch_global            = %{$switch_connection->{switch_db}};
2507
2508   my %db_building= ();
2509   for my $sw (@SWITCH_LIST) {
2510      my ($building, $location) = split m/ \/ /xms, $sw->{location}, 2;
2511      $db_building{$building} ||= {};
2512      $db_building{$building}->{$location} ||= {};
2513      $db_building{$building}->{$location}{ $sw->{hostname} } = 'y';
2514      }
2515
2516
2517   print "digraph G {\n";
2518   print "rankdir = LR;\n";
2519   #print "splines=polyline;\n";
2520
2521   print "site [label = \"site\", color = black, fillcolor = gold, shape = invhouse, style = filled];\n";
2522   print "internet [label = \"internet\", color = black, fillcolor = cyan, shape = house, style = filled];\n";
2523
2524   my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime time;
2525   $year += 1900;
2526   $mon++;
2527   my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
2528   print "\"$date\" [ color = white, fillcolor = black, shape = polygon, sides=14, style = filled, fontcolor = white ]\n";
2529   print "site -> \"$date\" [color = white];\n";
2530
2531   my $b=0;
2532   for my $building (keys %db_building) {
2533      $b++;
2534
2535      print "\"building$b\" [label = \"$building\", color = black, fillcolor = gold, style = filled];\n";
2536      print "site -> \"building$b\" [len = 2, color = firebrick];\n";
2537
2538      my $l = 0;
2539      for my $loc (keys %{$db_building{$building}}) {
2540         $l++;
2541
2542         print "\"location$b-$l\" [label = \"$building" . q{/} . join(q{\n}, split(m{ / }xms, $loc)) . "\", color = black, fillcolor = orange, style = filled];\n";
2543#         print "\"location$b-$l\" [label = \"$building / $loc\", color = black, fillcolor = orange, style = filled];\n";
2544         print "\"building$b\" -> \"location$b-$l\" [len = 2, color = firebrick]\n";
2545
2546         for my $sw (keys %{$db_building{$building}->{$loc}}) {
2547
2548            print "\"$sw:$db_switch_output_port{$sw}\" [label = \"$db_switch_output_port{$sw}\", color = black, fillcolor = lightblue,  peripheries = 2, style = filled];\n";
2549
2550            my $swname  = $sw;
2551               $swname .= q{\n-\n} . "$db_switch_global{$sw}->{model}" if exists $db_switch_global{$sw} and exists $db_switch_global{$sw}->{model};
2552            print "\"$sw\" [label = \"$swname\", color = black, fillcolor = palegreen, shape = rect, style = filled];\n";
2553            print "\"location$b-$l\" -> \"$sw\" [len = 2, color = firebrick, arrowtail = dot]\n";
2554            print "\"$sw\" -> \"$sw:$db_switch_output_port{$sw}\" [len=2, style=bold, arrowhead = normal, arrowtail = invdot]\n";
2555
2556
2557            for my $swport (keys %db_switch_connected_on_port) {
2558               my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
2559               next if not $sw_connect eq $sw;
2560               next if $port_connect eq $db_switch_output_port{$sw};
2561               print "\"$sw:$port_connect\" [label = \"$port_connect\", color = black, fillcolor = plum,  peripheries = 1, style = filled];\n";
2562               print "\"$sw:$port_connect\" -> \"$sw\" [len=2, style=bold, arrowhead= normal, arrowtail = inv]\n";
2563              }
2564            }
2565         }
2566      }
2567
2568#   print "Switch output port and parent port connection\n";
2569#   print "---------------------------------------------\n";
2570   for my $sw (sort keys %db_switch_output_port) {
2571      if (exists $db_switch_parent{$sw}) {
2572#         printf "   \"%s:%s\" -> \"%s:%s\"\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{switch}, $db_switch_parent{$sw}->{port};
2573         }
2574      else {
2575         printf "   \"%s:%s\" -> internet\n", $sw, $db_switch_output_port{$sw};
2576         }
2577      }
2578   print "\n";
2579
2580#   print "Switch parent and children port inter-connection\n";
2581#   print "------------------------------------------------\n";
2582   for my $swport (sort keys %db_switch_connected_on_port) {
2583      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
2584      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
2585         if (exists $db_switch_output_port{$sw}) {
2586            printf "   \"%s:%s\" -> \"%s:%s\" [color = navyblue]\n", $sw, $db_switch_output_port{$sw}, $sw_connect, $port_connect;
2587            }
2588         else {
2589            printf "   \"%s\"   -> \"%s%s\"\n", $sw, $sw_connect, $port_connect;
2590            }
2591         }
2592      }
2593
2594print "}\n";
2595   return;
2596   }
2597
2598
2599__END__
2600
2601=head1 NAME
2602
2603klask - ports manager and finder for switch
2604
2605
2606=head1 USAGE
2607
2608 klask updatedb [--verbose] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l]
2609 klask exportdb --format [txt|html]
2610 klask removedb computer*
2611 klask cleandb  [--verbose] --day number_of_day --repair-dns
2612
2613 klask updatesw [--verbose]
2614 klask exportsw --format [txt|dot]
2615
2616 klask searchdb --kind [host|mac] computer [mac-address]
2617 klask search   computer
2618 klask search-mac-on-switch [--verbose] [--vlan|-i vlan-id] switch mac_addr
2619
2620 klask ip-free  [--verbose] --day number_of_day --format [txt|html] [vlan_name]
2621
2622 klask enable  switch port
2623 klask disable switch port
2624 klask status  switch port
2625
2626
2627=head1 DESCRIPTION
2628
2629Klask is a small tool to find where is a host in a big network.
2630Klask mean search in brittany.
2631No hight level protocol like CDL, LLDP are use.
2632Everything is just done with SNMP request on MAC address.
2633
2634Limitation : loop cannot be detected and could be problematic when the map is created (C<updatesw> method).
2635If you use PVST or MSTP and create loop between VLAN,
2636you have to use C<portignore> functionality on switch port to cut manually loop
2637(see config file below).
2638
2639When you use a management port to administrate a switch,
2640it's not possible to create the map with this switch because it does not have a MAC address,
2641so other switch cannot find the real downlink port...
2642One way to work around this problem is, if you have a computer directly connected on the switch,
2643to put this IP as the fake ip for the switch.
2644The MAC address associated will be use just for the map detection.
2645The C<fake-ip> parameter is defined in the config file.
2646
2647Klask has now a web site dedicated for it !
2648
2649 http://servforge.legi.grenoble-inp.fr/projects/klask
2650
2651
2652=head1 COMMANDS
2653
2654
2655=head2 search
2656
2657This command takes one or more computer in argument.
2658It search a computer on the network and give the port and the switch on which the computer is connected.
2659
2660
2661=head2 enable
2662
2663This command activate a port on a switch by SNMP.
2664So you need to give the switch and the port number on the command line.
2665Actually, port number must be a number and not the name alias...
2666
2667
2668=head2 disable
2669
2670This command deactivate a port on a switch by SNMP.
2671So you need to give the switch and the port number on the command line.
2672Actually, port number must be a number and not the name alias...
2673
2674
2675=head2 status
2676
2677This command return the status of a port number on a switch by SNMP.
2678So you need to give the switch name and the port number on the command line.
2679Actually, port number must be a number and not the name alias...
2680
2681
2682=head2 updatedb
2683
2684This command will scan networks and update a database.
2685To know which are the cmputer scan, you have to configure the file F</etc/klask/klask.conf>.
2686This file is easy to read and write because klask use YAML format and not XML.
2687
2688
2689=head2 exportdb --format [txt|html]
2690
2691This command print the content of the database. There is actually only one format.
2692It's very easy to have more format, it's just need times...
2693
2694=head2 removedb
2695
2696This command remove an entry in the database.
2697There is only one kind of parameter, the IP of the computers to be removed.
2698You can put as many IP as you want...
2699
2700DNS computer name could also a valid entry because a DNS resolver is done at the beginning.
2701
2702=head2 cleandb
2703
2704Remove double entry (same MAC-Address) when the older one is older than X day (C<--day>) the new one.
2705Computer name beginning by 'float' are not really taken into account but could be remove.
2706
2707When reverse DNS has not be done, option --repair-dns force reverse DNS check on unkown host.
2708
2709=head2 updatesw
2710
2711This command build a map of your manageable switch on your network.
2712The list of the switch must be given in the file F</etc/klask/klask.conf>.
2713
2714
2715=head2 exportsw --format [txt|dot]
2716
2717This command print the content of the switch database. There is actually two format.
2718One is just txt for terminal and the other is the dot format from the graphviz environnement.
2719
2720 klask exportsw --format dot > /tmp/map.dot
2721 dot -Tpng /tmp/map.dot > /tmp/map.png
2722
2723
2724
2725=head1 CONFIGURATION
2726
2727Because klask need many parameters, it's not possible actually to use command line parameters for everything.
2728The configuration is done in a F</etc/klask/klask.conf> YAML file.
2729This format have many advantage over XML, it's easier to read and to write !
2730
2731Here an example, be aware with indent, it's important in YAML, do not use tabulation !
2732
2733 default:
2734   community: public
2735   snmpport: 161
2736
2737 network:
2738   labnet:
2739     ip-subnet:
2740       - add: 192.168.1.0/24
2741       - add: 192.168.2.0/24
2742     interface: eth0
2743     vlan-id: 12
2744     main-router: gw1.labnet.local
2745
2746   schoolnet:
2747     ip-subnet:
2748       - add: 192.168.6.0/24
2749       - add: 192.168.7.0/24
2750     interface: eth0.38
2751     vlan-id: 13
2752     main-router: gw2.schoolnet.local
2753
2754 switch:
2755   - hostname: sw1.klask.local
2756     portignore:
2757       - 1
2758       - 2
2759
2760   - hostname: sw2.klask.local
2761     location: BatK / 2 / K203
2762     type: HP2424
2763     portignore:
2764       - 1
2765       - 2
2766     fake-ip: 192.168.9.14
2767
2768I think it's pretty easy to understand.
2769The default section can be overide in any section, if parameter mean something in theses sections.
2770Network to be scan are define in the network section. You must put an add by network.
2771Maybe I will make a delete line to suppress specific computers.
2772The switch section define your switch.
2773You have to write the port number to ignore, this was important if your switchs are cascade
2774(right now, method C<updatesw> find them automatically)
2775and is still important if you have loop (with PVST or MSTP).
2776Just put the ports numbers between switch.
2777
2778
2779=head1 FILES
2780
2781 /etc/klask/klask.conf
2782 /var/lib/klask/klaskdb
2783 /var/lib/klask/switchdb
2784
2785=head1 SEE ALSO
2786
2787Net::SNMP, Net::Netmask, Net::CIDR::Lite, NetAddr::IP, YAML
2788
2789
2790=head1 VERSION
2791
2792$Id: klask 215 2017-02-18 13:25:55Z g7moreau $
2793
2794
2795=head1 AUTHOR
2796
2797Written by Gabriel Moreau, Grenoble - France
2798
2799
2800=head1 LICENSE AND COPYRIGHT
2801
2802GPL version 2 or later and Perl equivalent
2803
2804Copyright (C) 2005-2017 Gabriel Moreau.
Note: See TracBrowser for help on using the repository browser.