source: trunk/klask @ 344

Last change on this file since 344 was 344, checked in by g7moreau, 6 years ago
  • Many small improvment and error &nbsp ->   ...
  • Property svn:executable set to *
  • Property svn:keywords set to Date Author Id Rev
File size: 137.3 KB
Line 
1#!/usr/bin/perl -w
2#
3# Copyright (C) 2005-2017 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>
4#
5# $Id: klask 344 2017-11-01 06:24:34Z g7moreau $
6
7use strict;
8use warnings;
9use version; our $VERSION = qv('0.7.6');
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';
22use Digest::SHA qw(sha512_base64);
23use Text::Table; # libtext-table-perl http://blogs.perl.org/users/steven_haryanto/2014/07/benchmarking-several-ascii-table-generator-modules.html
24
25# apt-get install snmp fping libnet-cidr-lite-perl libnet-netmask-perl libnet-snmp-perl libnetaddr-ip-perl libyaml-perl
26# libcrypt-des-perl libcrypt-hcesha-perl libdigest-hmac-perl libtext-table-perl
27# arping net-tools fping bind9-host arpwatch
28
29################################################################
30# general initialization
31################################################################
32
33my $KLASK_VAR      = '/var/lib/klask';
34my $KLASK_CFG_FILE = '/etc/klask/klask.conf';
35my $KLASK_DB_FILE  = "$KLASK_VAR/klaskdb";
36my $KLASK_SW_FILE  = "$KLASK_VAR/switchdb";
37
38test_running_environnement();
39
40my $KLASK_CFG = YAML::Syck::LoadFile("$KLASK_CFG_FILE");
41
42my %DEFAULT     = %{$KLASK_CFG->{'default'}};
43my @SWITCH_LIST = @{$KLASK_CFG->{'switch'}};
44
45my %SWITCH_LEVEL = ();
46my %SWITCH_DB    = ();
47LEVEL_OF_EACH_SWITCH:
48for my $sw (@SWITCH_LIST) {
49   $SWITCH_LEVEL{$sw->{'hostname'}} = $sw->{'level'} || $DEFAULT{'switch_level'}  || 2;
50   $SWITCH_DB{$sw->{'hostname'}} = $sw;
51
52   # SNMP parameter initialisation
53   my %session = ( -hostname   => $sw->{'hostname'} );
54   $session{-version} = $sw->{'version'}  || 1;
55   $session{-port}    = $sw->{'snmpport'} || $DEFAULT{'snmpport'}  || 161;
56   if (exists $sw->{'version'} and $sw->{'version'} eq '3') {
57      $session{-username} = $sw->{'username'} || 'snmpadmin';
58      }
59   else {
60      $session{-community} = $sw->{'community'} || $DEFAULT{'community'} || 'public';
61      }
62   $sw->{'snmp_param_session'} = \%session;
63   }
64@SWITCH_LIST = reverse sort { $SWITCH_LEVEL{$a->{'hostname'}} <=> $SWITCH_LEVEL{$b->{'hostname'}} } @{$KLASK_CFG->{'switch'}};
65
66#my %SWITCH_PORT_COUNT = ();
67
68my %CMD_DB = (
69   'help'                 => \&cmd_help,
70   'version'              => \&cmd_version,
71   'exportdb'             => \&cmd_exportdb,
72   'updatedb'             => \&cmd_updatedb,
73   'searchdb'             => \&cmd_searchdb,
74   'removedb'             => \&cmd_removedb,
75   'cleandb'              => \&cmd_cleandb,
76   'search'               => \&cmd_search,
77   'enable'               => \&cmd_enable,
78   'disable'              => \&cmd_disable,
79   'status'               => \&cmd_status,
80   'updatesw'             => \&cmd_updatesw,
81   'exportsw'             => \&cmd_exportsw,
82   'iplocation'           => \&cmd_ip_location,
83   'ip-free'              => \&cmd_ip_free,
84   'search-mac-on-switch' => \&cmd_search_mac_on_switch,
85   'bad-vlan-id'          => \&cmd_bad_vlan_id,
86   'poe-enable'           => \&cmd_poe_enable,
87   'poe-disable'          => \&cmd_poe_disable,
88   'poe-status'           => \&cmd_poe_status,
89   'port-setvlan'         => \&cmd_port_setvlan,
90   'port-getvlan'         => \&cmd_port_getvlan,
91   'vlan-setname'         => \&cmd_vlan_setname,
92   'vlan-getname'         => \&cmd_vlan_getname,
93   'vlan-list'            => \&cmd_vlan_list,
94   'host-setlocation'     => \&cmd_host_setlocation,
95   'rebootsw'             => \&cmd_rebootsw,
96   );
97
98#Readonly my %INTERNAL_PORT_MAP => (
99#   0 => 'A',
100#   1 => 'B',
101#   2 => 'C',
102#   3 => 'D',
103#   4 => 'E',
104#   5 => 'F',
105#   6 => 'G',
106#   7 => 'H',
107#   );
108#Readonly my %INTERNAL_PORT_MAP_REV => reverse %INTERNAL_PORT_MAP;
109
110Readonly my %SWITCH_KIND => (
111   # HP
112   J3299A           => { type => 1, model => 'HP224M',         match => 'HP J3299A ProCurve Switch 224M',      revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
113   J4120A           => { type => 1, model => 'HP1600M',        match => 'HP J4120A ProCurve Switch 1600M',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
114   J9029A           => { type => 1, model => 'HP1800-8G',      match => 'PROCURVE J9029A',                     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
115   J9449A           => { type => 1, model => 'HP1810-8G',      match => 'HP ProCurve 1810G - 8 GE',            revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
116   J4093A           => { type => 1, model => 'HP2424M',        match => 'HP J4093A ProCurve Switch 2424M',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
117   J9279A           => { type => 1, model => 'HP2510G-24',     match => 'ProCurve J9279A Switch 2510G-24',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
118   J9280A           => { type => 1, model => 'HP2510G-48',     match => 'ProCurve J9280A Switch 2510G-48',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
119   J4813A           => { type => 1, model => 'HP2524',         match => 'HP J4813A ProCurve Switch 2524',      revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
120   J4900A           => { type => 1, model => 'HP2626A',        match => 'HP J4900A ProCurve Switch 2626',      revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
121   J4900B           => { type => 1, model => 'HP2626B',        match => 'J4900B.+?Switch 2626',                revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   }, # ProCurve J4900B Switch 2626 # HP J4900B ProCurve Switch 2626
122   J4899B           => { type => 1, model => 'HP2650',         match => 'ProCurve J4899B Switch 2650',         revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
123   J9021A           => { type => 1, model => 'HP2810-24G',     match => 'ProCurve J9021A Switch 2810-24G',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
124   J9022A           => { type => 1, model => 'HP2810-48G',     match => 'ProCurve J9022A Switch 2810-48G',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
125   J8692A           => { type => 1, model => 'HP3500-24G',     match => 'J8692A Switch 3500yl-24G',            revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
126   J4903A           => { type => 1, model => 'HP2824',         match => 'J4903A.+?Switch 2824,',               revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
127   J4110A           => { type => 1, model => 'HP8000M',        match => 'HP J4110A ProCurve Switch 8000M',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
128   JE074A           => { type => 2, model => 'HP5120-24G',     match => 'HP Comware.+?A5120-24G EI Switch',    revision => qr{Comware .*? Version (\d[\d\.]+?) Release},   },
129   JE069A           => { type => 2, model => 'HP5120-48G',     match => 'HP Comware.+?A5120-48G EI Switch',    revision => qr{Comware .*? Version (\d[\d\.]+?) Release},   },
130   JD377A           => { type => 2, model => 'HP5500-24G',     match => 'HP Comware.+?A5500-24G EI Switch',    revision => qr{Comware .*? Version (\d[\d\.]+?) Release},   },
131   JD374A           => { type => 2, model => 'HP5500-24F',     match => 'HP Comware.+?A5500-24G-SFP EI ',      revision => qr{Comware .*? Version (\d[\d\.]+?) Release},   },
132   # BayStack
133   BS350T           => { type => 1, model => 'BS350T',         match => 'BayStack 350T HW'                     },
134   # Nexans
135   N3483G           => { type => 2, model => 'NA3483-6G',      match => 'GigaSwitch V3 TP SFP-I 48.+ ES3',     revision => qr{GigaSwitch .*?/SECURITY/V(\d[\d\.]+\w?)\)},  },
136   N3483P           => { type => 2, model => 'NA3483-6P',      match => 'GigaSwitch V3 TP.PSE.+ 48/54V ES3',   revision => qr{GigaSwitch .*?/SECURITY/V(\d[\d\.]+\w?)\)},  }, # GigaSwitch V3 TP(PSE+) SFP-I 48/54V ES3 (HW3/ENHANCED/SECURITY/V4.10C)
137   # DELL
138   PC7024           => { type => 2, model => 'DPC7024',        match => 'PowerConnect 7024,.+?VxWorks',        revision => qr{PowerConnect .*?, (\d[\d\.]+?), VxWorks},    },
139   N2048            => { type => 2, model => 'DN2048',         match => 'Dell Networking N2048,',              revision => qr{Dell Networking .*?, (\d[\d\.]+?), Linux},   },
140   N4032F           => { type => 2, model => 'DN4032F',        match => 'Dell Networking N4032F,',             revision => qr{Dell Networking .*?, (\d[\d\.]+?), Linux},   },
141   N4064F           => { type => 2, model => 'DN4064F',        match => 'Dell Networking N4064F,',             revision => qr{Dell Networking .*?, (\d[\d\.]+?), Linux},   },
142   # 3COM
143   'H3C5500'        => { type => 1, model => 'H3C5500',        match => 'H3C S5500-SI Series'                  },
144   '3C17203'        => { type => 1, model => '3C17203',        match => '3Com SuperStack 3 24-Port'            },
145   '3C17204'        => { type => 1, model => '3C17204',        match => '3Com SuperStack 3 48-Port'            },
146   '3CR17562-91'    => { type => 1, model => '3CR17562-91',    match => '3Com Switch 4500 50-Port'             },
147   '3CR17255-91'    => { type => 1, model => '3CR17255-91',    match => '3Com Switch 5500G-EI 48-Port'         },
148   '3CR17251-91'    => { type => 1, model => '3CR17251-91',    match => '3Com Switch 5500G-EI 48-Port'         },
149   '3CR17571-91'    => { type => 1, model => '3CR17571-91',    match => '3Com Switch 4500 PWR 26-Port'         },
150   '3CRWX220095A'   => { type => 1, model => '3CRWX220095A',   match => '3Com Wireless LAN Controller'         },
151   '3CR17254-91'    => { type => 1, model => '3CR17254-91',    match => '3Com Switch 5500G-EI 24-Port'         },
152   '3CRS48G-24S-91' => { type => 1, model => '3CRS48G-24S-91', match => '3Com Switch 4800G 24-Port'            },
153   '3CRS48G-48S-91' => { type => 1, model => '3CRS48G-48S-91', match => '3Com Switch 4800G 48-Port'            },
154   '3C17708'        => { type => 1, model => '3C17708',        match => '3Com Switch 4050'                     },
155   '3C17709'        => { type => 1, model => '3C17709',        match => '3Com Switch 4060'                     },
156   '3C17707'        => { type => 1, model => '3C17707',        match => '3Com Switch 4070'                     },
157   '3CR17258-91'    => { type => 1, model => '3CR17258-91',    match => '3Com Switch 5500G-EI 24-Port SFP'     },
158   '3CR17181-91'    => { type => 1, model => '3CR17181-91',    match => '3Com Switch 5500-EI 28-Port FX'       },
159   '3CR17252-91'    => { type => 1, model => '3CR17252-91',    match => '3Com Switch 5500G-EI PWR 24-Port'     },
160   '3CR17253-91'    => { type => 1, model => '3CR17253-91',    match => '3Com Switch 5500G-EI PWR 48-Port'     },
161   '3CR17250-91'    => { type => 1, model => '3CR17250-91',    match => '3Com Switch 5500G-EI 24-Port'         },
162   '3CR17561-91'    => { type => 1, model => '3CR17561-91',    match => '3Com Switch 4500 26-Port'             },
163   '3CR17572-91'    => { type => 1, model => '3CR17572-91',    match => '3Com Switch 4500 PWR 50-Port'         },
164   '3C17702-US'     => { type => 1, model => '3C17702-US',     match => '3Com Switch 4900 SX'                  },
165   '3C17700'        => { type => 1, model => '3C17700',        match => '3Com Switch 4900'                     },
166   );
167
168Readonly my %OID_NUMBER => (
169   sysDescription  => '1.3.6.1.2.1.1.1.0',
170   sysName         => '1.3.6.1.2.1.1.5.0',
171   sysContact      => '1.3.6.1.2.1.1.4.0',
172   sysLocation     => '1.3.6.1.2.1.1.6.0',
173   searchPort1     => '1.3.6.1.2.1.17.4.3.1.2',          # BRIDGE-MIB (802.1D).
174   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
175   vlanPortDefault => '1.3.6.1.2.1.17.7.1.4.5.1.1',      # dot1qPvid
176   vlanStatus      => '1.3.6.1.2.1.17.7.1.4.3.1.5',      # integer 4 Create, 6 Destroy
177   vlanName        => '1.3.6.1.2.1.17.7.1.4.3.1.1',      # string
178   HPicfReset      => '1.3.6.1.4.1.11.2.14.11.1.4.1',    # HP reboot switch
179   ifIndex         => '1.3.6.1.2.1.17.1.4.1.2',          # dot1dBasePortIfIndex - Interface index redirection
180   ifName          => '1.3.6.1.2.1.31.1.1.1.1',          # Interface name (give port number)
181   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)
182   poeState        => '1.3.6.1.2.1.105.1.1.1.3.1',       # 1.3.6.1.2.1.105.1.1.1.3.1.NoPort = 1 (poe up)  = 2 (poe down) - Cisco and Zyxel
183   NApoeState      => '1.3.6.1.4.1.266.20.3.1.1.21',     # .NoPort = 2 (poe off)  = 8 (poe atHighPower) - Nexans
184   ifAggregator    => '1.2.840.10006.300.43.1.2.1.1.12', # dot3adAggPortSelectedAggID - 0 not part of an  Aggregator - Ciso Dell HP Comware -  See https://stackoverflow.com/questions/14960157/how-to-map-portchannel-to-interfaces-via-snmp https://gist.github.com/bldewolf/6314435
185   );
186
187Readonly my %PORT_UPDOWN => (
188   1 => 'enable',
189   2 => 'disable',
190   );
191
192Readonly 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;
193Readonly my $RE_IPv4_ADDRESS => qr{ [0-9]{1,3} \. [0-9]{1,3} \. [0-9]{1,3} \. [0-9]{1,3} }xms;
194
195Readonly my $RE_FLOAT_HOSTNAME => $DEFAULT{'float-regex'} || qr{ ^float }xms;
196
197Readonly my $SEP_AGGREGATOR_PORT => ',';  # : is already use to join switch and port
198Readonly my $SEP_SWITCH_PORT     => ':';
199
200
201################################################################
202# main program
203################################################################
204
205my $cmd = shift @ARGV || 'help';
206if (defined $CMD_DB{$cmd}) {
207   $CMD_DB{$cmd}->(@ARGV);
208   }
209else {
210   print {*STDERR} "klask: command $cmd not found\n\n";
211   $CMD_DB{'help'}->();
212   exit 1;
213   }
214
215exit;
216
217################################################################
218# subroutine
219################################################################
220
221#---------------------------------------------------------------
222sub test_running_environnement {
223   die "Configuration file $KLASK_CFG_FILE does not exists. Klask need it !\n" if not -e "$KLASK_CFG_FILE";
224   die "Var folder $KLASK_VAR does not exists. Klask need it !\n"              if not -d "$KLASK_VAR";
225   return;
226   }
227
228#---------------------------------------------------------------
229sub test_switchdb_environnement {
230   die "Switch database $KLASK_SW_FILE does not exists. Launch updatesw before this command !\n" if not -e "$KLASK_SW_FILE";
231   return;
232   }
233
234#---------------------------------------------------------------
235sub test_maindb_environnement {
236   die "Main database $KLASK_DB_FILE does not exists. Launch updatedb before this command !\n" if not -e "$KLASK_DB_FILE";
237   return;
238   }
239
240#---------------------------------------------------------------
241# fast ping dont l'objectif est de remplir la table arp de la machine
242sub fast_ping {
243   # Launch this command without waiting...
244   system "fping -q -c 1 @_ > /dev/null 2>&1 &";
245   return;
246   }
247
248#---------------------------------------------------------------
249sub shell_command {
250   my $cmd = shift;
251
252   my $fh     = new FileHandle;
253   my $result = '';
254   open $fh, q{-|}, "LANG=C $cmd" or die "Can't exec $cmd\n";
255   $result .= <$fh>;
256   close $fh;
257   chomp $result;
258   return $result;
259   }
260
261#---------------------------------------------------------------
262# donne l'@ ip, dns, arp en fonction du dns OU de l'ip
263sub resolve_ip_arp_host {
264   my $param_ip_or_host = shift;
265   my $interface = shift || q{*};
266   my $type      = shift || q{fast};
267   my $already   = shift || q{yes};
268
269   my %ret = (
270      hostname_fq  => 'unknow',
271      ipv4_address => '0.0.0.0',
272      mac_address  => 'unknow',
273      );
274
275   # perl -MSocket -E 'say inet_ntoa(scalar gethostbyname("tech7meylan.hmg.inpg.fr"))'
276   my $packed_ip = scalar gethostbyname($param_ip_or_host);
277   return %ret if not defined $packed_ip;
278   $ret{'ipv4_address'} = inet_ntoa($packed_ip);
279   #if ($ret{'ipv4_address'} !~ m/$RE_IPv4_ADDRESS/) {
280   #   print "Error: for computer $param_ip_or_host on interface $interface, IP $ret{'ipv4_address'} is not valide\n";
281   #   return %ret;
282   #   }
283
284   # perl -MSocket -E 'say scalar gethostbyaddr(inet_aton("194.254.66.240"), AF_INET)'
285   my $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET) if $already eq 'yes';
286   $ret{'hostname_fq'} = $hostname_fq if defined $hostname_fq;
287
288   # my $cmd = q{grep  -he '\b} . $param_ip_or_host . q{\b' } . "/var/lib/arpwatch/$interface.dat | sort -rn -k 3,3 | head -1";
289   #my $cmd = q{grep  -he '\b} . $ret{'ipv4_address'} . q{\b' } . "/var/lib/arpwatch/$interface.dat | sort -rn -k 3,3 | head -1";
290   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';
291   #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
292
293   my $cmd_arpwatch = shell_command $cmd;
294   #my ($arp, $ip, $timestamp, $host) = split m/ \s+ /xms, $cmd_arpwatch;
295   my ($interface2, $arp, $ip, $timestamp, $host) = split m/ \s+ /xms, $cmd_arpwatch;
296
297   $ret{'interface'}    = $interface2 || $interface;
298   $ret{'mac_address'}  = $arp       if $arp;
299   $ret{'timestamp'}    = $timestamp if $timestamp;
300
301   my $nowtimestamp = time;
302
303   if ( $type eq 'fast' and ( not defined $timestamp or $timestamp < ( $nowtimestamp - 45 * 60 ) ) ) { # 45 min
304      $ret{'mac_address'} = 'unknow';
305      return %ret;
306      }
307
308   # ARP result
309   #
310   # LANG=C arp -a 194.254.66.62 -i eth331
311   # gw66-62.legi.grenoble-inp.fr (194.254.66.62) at 00:08:7c:bb:0f:c0 [ether] on eth331
312   #
313   # LANG=C ip neigh show to 194.254.66.62 dev eth331
314   # 194.254.66.62 lladdr 00:08:7c:bb:0f:c0 REACHABLE
315   # LANG=C ip neigh show to 194.254.66.62
316   # 194.254.66.62 dev eth331 lladdr 00:08:7c:bb:0f:c0 REACHABLE
317#   my $cmd_arp  = shell_command "arp -a $param_ip_or_host -i $ret{'interface'}";
318#   if ( $cmd_arp =~ m{ (\S*) \s \( ( $RE_IPv4_ADDRESS ) \) \s at \s ( $RE_MAC_ADDRESS ) }xms ) {
319#      ( $ret{'hostname_fq'}, $ret{'ipv4_address'}, $ret{'mac_address'} )  = ($1, $2, $3);
320#      }
321   if ($ret{'mac_address'} eq 'unknow') {
322      # Last chance to have the mac_address
323      if ($ret{'interface'} eq '*') {
324         my $cmd_arp  = shell_command "ip neigh show to $ret{'ipv4_address'}";
325         if ( $cmd_arp =~ m{ ^$RE_IPv4_ADDRESS \s dev \s ([\w\d\.\:]+) \s lladdr \s ( $RE_MAC_ADDRESS ) \s }xms ) {
326            ($ret{'interface'}, $ret{'mac_address'}) = ($1, $2);
327            }
328         }
329      else {
330         my $cmd_arp  = shell_command "ip neigh show to $ret{'ipv4_address'} dev $ret{'interface'}";
331         if ( $cmd_arp =~ m{ ^$RE_IPv4_ADDRESS \s lladdr \s ( $RE_MAC_ADDRESS ) \s }xms ) {
332            $ret{'mac_address'} = $1;
333            }
334         }
335      }
336
337   # Normalize MAC Address
338   if ($ret{'mac_address'} ne 'unknow') {
339      my @paquets = ();
340      for ( split m/ : /xms, $ret{'mac_address'} ) {
341         my @chars = split m//xms, uc "00$_";
342         push @paquets, "$chars[-2]$chars[-1]";
343         }
344      $ret{'mac_address'} = join q{:}, @paquets;
345      }
346
347   return %ret;
348   }
349
350#---------------------------------------------------------------
351# Find Surname of a switch
352sub get_switch_model {
353   my $sw_snmp_description = shift || 'unknow';
354   $sw_snmp_description =~ s/[\n\r]/ /g;
355
356   for my $sw_kind (keys %SWITCH_KIND) {
357      next if not $sw_snmp_description =~ m/$SWITCH_KIND{$sw_kind}->{'match'}/ms; # option xms break search, why ?
358
359      return $SWITCH_KIND{$sw_kind}->{'model'};
360      }
361
362   return $sw_snmp_description;
363   }
364
365#---------------------------------------------------------------
366# Find Revision Firmware of a switch
367sub get_switch_revision {
368   my $sw_snmp_description = shift || 'unknow';
369   $sw_snmp_description =~ s/[\n\r]/ /g;
370
371   for my $sw_kind (keys %SWITCH_KIND) {
372      next if not $sw_snmp_description =~ m/$SWITCH_KIND{$sw_kind}->{'match'}/ms; # option xms break search, why ?
373      last if not exists $SWITCH_KIND{$sw_kind}->{'revision'};
374
375      my ($revision) = $sw_snmp_description =~ m/$SWITCH_KIND{$sw_kind}->{'revision'}/xms;
376      return $revision || 'unknow';
377      }
378
379   return 'unknow';
380   }
381
382#---------------------------------------------------------------
383# Get switch name and switch model
384sub init_switch_names {
385   my ($verbose, $verb_description, $check_hostname, $check_location) = @_;
386
387   printf "%-26s                %-25s %s\n",'Switch','Description','Type(Revision)' if $verbose;
388   print "----------------------------------------------------------------------------------\n" if $verbose;
389
390   INIT_EACH_SWITCH:
391   for my $sw (my @CLONE = @SWITCH_LIST) { # Make a local clone because some element can be deleted
392      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
393      print "$error \n" if $error;
394
395      my $result = $session->get_request(
396         -varbindlist => [
397            $OID_NUMBER{'sysDescription'},
398            $OID_NUMBER{'sysName'},
399            $OID_NUMBER{'sysContact'},
400            $OID_NUMBER{'sysLocation'},
401            ]
402         );
403      if (!defined $result) {
404         printf {*STDERR} "ERROR: %s.\n", $session->error();
405         $session->close();
406         # Remove bad switch
407         @SWITCH_LIST = grep { $_->{'hostname'} ne $sw->{'hostname'} } @SWITCH_LIST;
408         delete $SWITCH_LEVEL{$sw->{'hostname'}} if exists $SWITCH_LEVEL{$sw->{'hostname'}};
409         delete $SWITCH_DB{$sw->{'hostname'}}    if exists $SWITCH_DB{$sw->{'hostname'}};
410         next INIT_EACH_SWITCH;
411         }
412
413      $sw->{'description'} = $result->{$OID_NUMBER{'sysName'}} || $sw->{'hostname'};
414      $sw->{'model'} = get_switch_model($result->{$OID_NUMBER{'sysDescription'}});
415      $sw->{'revision'} = get_switch_revision($result->{$OID_NUMBER{'sysDescription'}});
416      printf "%-26s 0--------->>>> %-25s %s\n", $sw->{'hostname'}, $sw->{'description'}, $sw->{'model'}.'('.$sw->{'revision'}.')' if $verbose;
417
418      if ($verb_description) {
419         my $desc = $result->{$OID_NUMBER{'sysDescription'}};
420         $desc =~ s/[\n\r]/ /g;
421         print " +> $sw->{'hostname'} - description: $desc\n";
422         }
423      if ($check_hostname) {
424         my ($hostname) = split /\./, $sw->{'hostname'}, 2;
425         print " +> $hostname - error internal hostname: $sw->{'hostname'}\n" if $result->{$OID_NUMBER{'sysName'}} ne $hostname;
426         }
427      if ($check_location) {
428         my $location = $result->{$OID_NUMBER{'sysLocation'}};
429         $location =~ s/^"(.+)"$/$1/;
430         print " +> $sw->{'hostname'} - error location: '$location' -> '$sw->{'location'}'\n" if $location ne $sw->{'location'};
431         }
432
433      $session->close;
434      }
435
436   print "\n" if $verbose;
437   return;
438   }
439
440#---------------------------------------------------------------
441# convert hexa (only 2 digits) to decimal
442sub digit_hex2dec {
443   #00:0F:1F:43:E4:2B
444   my $car = '00' . uc shift;
445
446   return '00' if $car eq '00UNKNOW';
447   my %table = (
448      '0'=>'0',  '1'=>'1',  '2'=>'2',  '3'=>'3',  '4'=>'4',
449      '5'=>'5',  '6'=>'6',  '7'=>'7',  '8'=>'8',  '9'=>'9',
450      'A'=>'10', 'B'=>'11', 'C'=>'12', 'D'=>'13', 'E'=>'14', 'F'=>'15',
451      );
452   my @chars = split m//xms, $car;
453   return $table{$chars[-2]}*16 + $table{$chars[-1]};
454   }
455
456#---------------------------------------------------------------
457
458sub normalize_mac_address {
459   my $mac_address = shift;
460
461   # D07E-28D1-7AB8 or D07E.28D1.7AB8 or d07e28-d17ab8
462   if ($mac_address =~ m{^ (?: [0-9A-Fa-f]{4} [-\.]){2} [0-9A-Fa-f]{4} $}xms
463      or $mac_address =~ m{^ [0-9A-Fa-f]{6} - [0-9A-Fa-f]{6} $}xms
464      ) {
465      $mac_address =~ s/[-\.]//g;
466      return join q{:}, unpack('(A2)*', uc($mac_address));
467      }
468
469   return join q{:}, map { substr( uc("00$_"), -2) } split m/ [:-] /xms, $mac_address;
470   }
471
472#---------------------------------------------------------------
473# convert MAC hex address to decimal
474sub mac_address_hex2dec {
475   #00:0F:1F:43:E4:2B
476   my $mac_address = shift;
477
478   my @paquets = split m/ : /xms, $mac_address;
479   my $return = q{};
480   for (@paquets) {
481      $return .= q{.} . digit_hex2dec($_);
482      }
483   return $return;
484   }
485
486#---------------------------------------------------------------
487sub format_aggregator4html {
488   my $port_hr = shift;
489   $port_hr =~ s/($SEP_AGGREGATOR_PORT)/: /; # First occurence
490   $port_hr =~ s/($SEP_AGGREGATOR_PORT)/ /g; # Other occurence
491   return $port_hr;
492   }
493
494#---------------------------------------------------------------
495sub format_aggregator4dot {
496   my $port_hr = shift;
497   $port_hr =~ s/($SEP_AGGREGATOR_PORT)/ - /; # First occurence
498   $port_hr =~ s/($SEP_AGGREGATOR_PORT)/ /g;  # Other occurence
499   return $port_hr;
500   }
501
502#---------------------------------------------------------------
503# fqdn_html_breakable change it's parameter, use it like chomp perl function
504
505sub fqdn_html_breakable {
506   my $ref_fqdn = \($_[0]);
507   
508   # Add HTML <wbr> before every . to permit line break
509   $$ref_fqdn =~ s{\.}{.<wbr />}g;
510   }
511
512#---------------------------------------------------------------
513# return the port and the switch where the computer is connected
514sub find_switch_port {
515   my $mac_address     = shift;
516   my $switch_proposal = shift || q{};
517   my $vlan_id = shift || 0;
518
519   my %ret;
520   $ret{'switch_description'} = 'unknow';
521   $ret{'switch_port_id'} = '0';
522
523   return %ret if $mac_address eq 'unknow';;
524
525   my @switch_search = @SWITCH_LIST;
526   if ($switch_proposal ne q{}) {
527      for my $sw (@SWITCH_LIST) {
528         next if $sw->{'hostname'} ne $switch_proposal;
529         unshift @switch_search, $sw;
530         last;
531         }
532      }
533
534   my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
535   my $oid_search_port2 = $OID_NUMBER{'searchPort2'} .'.'. $vlan_id . mac_address_hex2dec($mac_address);
536
537   LOOP_ON_SWITCH:
538   for my $sw (@switch_search) {
539      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
540      print "$error \n" if $error;
541
542      my $result = $session->get_request(
543         -varbindlist => [$oid_search_port1]
544         );
545      if (not defined $result) {
546         $result = $session->get_request(
547            -varbindlist => [$oid_search_port2]
548            );
549         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
550         }
551
552      if (not (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance')) {
553         $session->close;
554         next LOOP_ON_SWITCH;
555         }
556
557      my $swport_id = $result->{$oid_search_port1};
558      my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);
559
560      $session->close;
561
562      # IMPORTANT !!
563      # ceci empeche la detection sur certains port ...
564      # en effet les switch sont relies entre eux par un cable reseau et du coup
565      # tous les arp de toutes les machines sont presentes sur ces ports (ceux choisis ici sont les miens)
566      # cette partie est a ameliore, voir a configurer dans l'entete
567      # 21->24 45->48
568      SWITCH_PORT_IGNORE:
569      for my $portignore (@{$sw->{'portignore'}}) {
570         next LOOP_ON_SWITCH if $swport_hr eq $portignore;
571         my ($swport_hr_limited) = split /$SEP_AGGREGATOR_PORT/, $swport_hr; # Beginning of the swith port (Aggregator)
572         next LOOP_ON_SWITCH if $swport_hr_limited eq $portignore;
573         }
574
575      $ret{'switch_hostname'}    = $sw->{'hostname'};
576      $ret{'switch_description'} = $sw->{'description'};
577      $ret{'switch_port_id'}     = $swport_id;
578      $ret{'switch_port_hr'}     = $swport_hr; # human readable
579
580      last LOOP_ON_SWITCH;
581      }
582   return %ret;
583   }
584
585#---------------------------------------------------------------
586# search all the port on all the switches where the computer is detected
587sub find_all_switch_port {
588   my $mac_address = shift;
589   my $vlan_id     = shift || 0;
590
591   my $ret = {};
592
593   return $ret if $mac_address eq 'unknow';
594
595   my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
596   my $oid_search_port2 = $OID_NUMBER{'searchPort2'} .'.'. $vlan_id . mac_address_hex2dec($mac_address);
597   LOOP_ON_ALL_SWITCH:
598   for my $sw (@SWITCH_LIST) {
599      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
600      print "$error \n" if $error;
601
602      my $result = $session->get_request(
603         -varbindlist => [$oid_search_port1]
604         );
605      if (not defined $result) {
606         $result = $session->get_request(
607            -varbindlist => [$oid_search_port2]
608            );
609         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
610         }
611
612      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
613         my $swport_id = $result->{$oid_search_port1};
614         my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);
615
616         SWITCH_PORT_IGNORE:
617         for my $portignore (@{$sw->{'portignore'}}) {
618            if ($swport_hr eq $portignore) {
619               $session->close;
620               next LOOP_ON_ALL_SWITCH
621               }
622            }
623
624         $ret->{$sw->{'hostname'}} = {};
625         $ret->{$sw->{'hostname'}}{'hostname'}    = $sw->{'hostname'};
626         $ret->{$sw->{'hostname'}}{'description'} = $sw->{'description'};
627         $ret->{$sw->{'hostname'}}{'port_id'}     = $swport_id;
628         $ret->{$sw->{'hostname'}}{'port_hr'}     = $swport_hr;
629         }
630
631      $session->close;
632      }
633   return $ret;
634   }
635
636#---------------------------------------------------------------
637sub get_list_network {
638
639   return keys %{$KLASK_CFG->{'network'}};
640   }
641
642#---------------------------------------------------------------
643sub get_current_interface {
644   my $vlan_name = shift;
645
646   return $KLASK_CFG->{'network'}{$vlan_name}{'interface'};
647   }
648
649#---------------------------------------------------------------
650sub get_current_vlan_id {
651   my $vlan_name = shift;
652
653   return 0 if not exists $KLASK_CFG->{'network'}{$vlan_name};
654   return $KLASK_CFG->{'network'}{$vlan_name}{'vlan-id'};
655   }
656
657#---------------------------------------------------------------
658sub get_current_scan_mode {
659   my $vlan_name = shift;
660
661   return $KLASK_CFG->{'network'}{$vlan_name}{'scan-mode'} || $DEFAULT{'scan-mode'} || 'active';
662   }
663
664#---------------------------------------------------------------
665sub get_current_vlan_name_for_interface {
666   my $interface = shift;
667
668   for my $vlan_name (keys %{$KLASK_CFG->{'network'}}) {
669      next if $KLASK_CFG->{'network'}{$vlan_name}{'interface'} ne $interface;
670      return $vlan_name;
671      }
672   }
673
674#---------------------------------------------------------------
675# liste l'ensemble des adresses ip d'un réseau
676sub get_list_ip {
677   my @vlan_name = @_;
678
679   my $cidrlist = Net::CIDR::Lite->new;
680
681   for my $net (@vlan_name) {
682      my @line  = @{$KLASK_CFG->{'network'}{$net}{'ip-subnet'}};
683      for my $cmd (@line) {
684         for my $method (keys %{$cmd}) {
685            $cidrlist->add_any($cmd->{$method}) if $method eq 'add';
686            }
687         }
688      }
689
690   my @res = ();
691
692   for my $cidr ($cidrlist->list()) {
693      my $net = new NetAddr::IP $cidr;
694      for my $ip (@{$net}) {
695         $ip =~ s{ /32 }{}xms;
696         push @res,  $ip;
697         }
698      }
699
700   return @res;
701   }
702
703#---------------------------------------------------------------
704# liste l'ensemble des routeurs du réseau
705sub get_list_main_router {
706   my @vlan_name = @_;
707
708   my @res = ();
709
710   for my $net (@vlan_name) {
711      push @res, $KLASK_CFG->{'network'}{$net}{'main-router'};
712      }
713
714   return @res;
715   }
716
717#---------------------------------------------------------------
718sub normalize_port_human_readable {
719   my $sw_port_hr  = shift;
720
721   # Manufacturer abbreviation
722   $sw_port_hr =~ s/^Bridge-Aggregation/Br/i;
723   $sw_port_hr =~ s/^Port-Channel/Po/i;
724   $sw_port_hr =~ s/^Forty-?GigabitEthernet/Fo/i;
725   $sw_port_hr =~ s/^Ten-?GigabitEthernet/Te/i;
726   $sw_port_hr =~ s/^GigabitEthernet/Gi/i;
727   $sw_port_hr =~ s/^FastEthernet/Fa/i;
728
729   # Customer abbreviation
730   $sw_port_hr =~ s/^Ten/Te/i;
731   $sw_port_hr =~ s/^Giga/Gi/i;
732
733   return ucfirst $sw_port_hr;
734   }
735
736#---------------------------------------------------------------
737sub snmp_get_rwsession {
738   my ($sw) = @_;
739
740   my %session = %{$sw->{'snmp_param_session'}};
741   $session{-community} = $sw->{'community-rw'} || $DEFAULT{'community-rw'} || 'private';
742   return %session;
743   }
744
745#---------------------------------------------------------------
746sub snmp_get_switchport_id2hr {
747   my ($snmp_session, $swport_id) = @_;
748
749   # On H3C, port id (port_id) and port index (port_ix) are not the same
750   # Double SNMP request to get the name
751   # First get the index, second get the name (port_hr)
752
753   my $oid_search_ix = $OID_NUMBER{'ifIndex'} .'.'. $swport_id;
754   my $result_ix = $snmp_session->get_request(
755      -varbindlist => [$oid_search_ix]
756      );
757
758   my $swport_ix = $swport_id;
759   $swport_ix = $result_ix->{$oid_search_ix} if defined $result_ix;
760
761   return snmp_get_switchport_ix2hr($snmp_session, $swport_ix);
762   }
763
764#---------------------------------------------------------------
765sub snmp_get_switchport_ix2hr {
766   my ($snmp_session, $swport_ix) = @_;
767
768   my $oid_search_hr = $OID_NUMBER{'ifName'} .'.'. $swport_ix;
769   my $result_hr = $snmp_session->get_request(
770      -varbindlist => [$oid_search_hr]
771      );
772   my $swport_hr = $swport_ix;
773   $swport_hr = normalize_port_human_readable($result_hr->{$oid_search_hr}) if defined $result_hr;
774
775   # Aggregator port
776   if ($swport_hr =~ m/^(Trk|Br|Po)/) {
777      my $oid_search_index = $OID_NUMBER{'ifAggregator'}; # base OID
778      my @args = ( -varbindlist =>  [$oid_search_index]);
779      LOOP_ON_OID_PORT:
780      while ( defined $snmp_session->get_next_request(@args) ) {
781         my ($oid_current) = $snmp_session->var_bind_names;
782         last LOOP_ON_OID_PORT if  not Net::SNMP::oid_base_match($oid_search_index, $oid_current);
783
784         # IEEE8023-LAG-MIB::dot3adAggPortSelectedAggID.28 = INTEGER: 337
785         # IEEE8023-LAG-MIB::dot3adAggPortAttachedAggID.28 = INTEGER: 337
786         my $port_aggregator_index = $snmp_session->var_bind_list->{$oid_current};
787         my ($current_port_ix) = reverse split /\./, $oid_current; # last number
788
789         # prepare next loop item
790         @args = (-varbindlist => [$oid_current]);
791
792         next LOOP_ON_OID_PORT if $port_aggregator_index == 0;
793         next LOOP_ON_OID_PORT if not $port_aggregator_index == $swport_ix;
794
795         my $current_port_name = snmp_get_switchport_ix2hr($snmp_session, $current_port_ix);
796         $swport_hr .= "$SEP_AGGREGATOR_PORT$current_port_name";
797         }
798      }
799   return $swport_hr;
800   }
801
802#---------------------------------------------------------------
803# Reverse search port number
804sub snmp_get_switchport_hr2id {
805   my ($snmp_session, $swport_hr, $verbose) = @_;
806
807   # Split for Aggregator port
808   # Keep only the Aggregator part
809   ($swport_hr) = split /$SEP_AGGREGATOR_PORT/, $swport_hr;
810
811   my $swport_id = $swport_hr;
812   # direct return if already numeric (next loop is expensive) / old or simple switch
813   return $swport_id if $swport_id =~ m/^\d+$/;
814
815   my $oid_search_ix = $OID_NUMBER{'ifIndex'}; # base OID
816   my @args = ( -varbindlist =>  [$oid_search_ix]);
817   LOOP_ON_OID_PORT:
818   while ( defined $snmp_session->get_next_request(@args) ) {
819      my ($oid_current) = $snmp_session->var_bind_names;
820      last LOOP_ON_OID_PORT if  not Net::SNMP::oid_base_match($oid_search_ix, $oid_current);
821
822      my $port_ifIndex = $snmp_session->var_bind_list->{$oid_current};
823      my ($port_ix) = reverse split /\./, $oid_current; # last number
824      printf "PORT1: %s => %s\n", $oid_current, $port_ifIndex if $verbose;
825
826      # prepare next loop item
827      @args = (-varbindlist => [$oid_current]);
828
829      my $oid_search_ifName = $OID_NUMBER{'ifName'} .'.'. $port_ifIndex;
830      my $result = $snmp_session->get_request(-varbindlist => [$oid_search_ifName]);
831      next LOOP_ON_OID_PORT if not defined $result;
832
833      my $current_port_hr = normalize_port_human_readable($result->{$oid_search_ifName});
834      printf "PORT2: $oid_search_ifName => $current_port_hr\n" if $verbose;
835      if ($current_port_hr eq $swport_hr) {
836         print "PORT3: $current_port_hr <-> $port_ix\n" if $verbose;
837
838         # return port number ifIndex need by OID portUpDown
839         $swport_id = $port_ifIndex; # other possible value could be $port_ix
840         last LOOP_ON_OID_PORT;
841         }
842      }
843   return $swport_id;
844   }
845
846#---------------------------------------------------------------
847# Get the list of all the VLAN define on a switch
848sub snmp_get_vlan_list {
849   my ($snmp_session, $verbose) = @_;
850
851   my %vlandb = (); # Hash vlan number => vlan name
852
853   my $oid_search_index = $OID_NUMBER{'vlanName'}; # base OID
854   my @args = ( -varbindlist =>  [$oid_search_index]);
855   LOOP_ON_VLAN:
856   while ( defined $snmp_session->get_next_request(@args) ) {
857      my ($oid_current) = $snmp_session->var_bind_names;
858      last LOOP_ON_VLAN if not Net::SNMP::oid_base_match($oid_search_index, $oid_current);
859
860      my $vlan_name = $snmp_session->var_bind_list->{$oid_current};
861      my ($vlan_id) = reverse split /\./, $oid_current; # last number
862      printf "VLAN: %s => %s\n", $oid_current, $vlan_name if $verbose;
863
864      $vlandb{$vlan_id} = $vlan_name;
865
866      # prepare next loop item
867      @args = (-varbindlist => [$oid_current]);
868      }
869   return %vlandb;
870   }
871
872#---------------------------------------------------------------
873# Load computer database
874sub computerdb_load {
875   my $computerdb = YAML::Syck::LoadFile("$KLASK_DB_FILE");
876
877   LOOP_ON_IP_ADDRESS:
878   for my $ip (keys %{$computerdb}) {
879
880      # Rename switch_port -> switch_port_id (2017/09/15)
881      if (not exists $computerdb->{$ip}{'switch_port_id' and exists $computerdb->{$ip}{'switch_port'}}) {
882         $computerdb->{$ip}{'switch_port_id'} = $computerdb->{$ip}{'switch_port'} if defined $computerdb->{$ip}{'switch_port'};
883         $computerdb->{$ip}{'switch_port_id'} = 0 if $computerdb->{$ip}{'switch_port_id'} !~ m/^\d+$/; # force numeric
884         }
885      delete $computerdb->{$ip}{'switch_port'} if exists $computerdb->{$ip}{'switch_port'};
886
887      next LOOP_ON_IP_ADDRESS if exists $computerdb->{$ip}{'switch_port_hr'} and defined $computerdb->{$ip}{'switch_port_hr'};
888
889      $computerdb->{$ip}{'switch_port_hr'} = $computerdb->{$ip}{'switch_port_id'};
890      }
891
892   return $computerdb;
893   }
894
895#---------------------------------------------------------------
896sub get_switchdb_checksum {
897   my %switch_db = @_; # same as global %SWITCH_DB
898
899   my $checksum_data = '';
900   for my $sw_name (sort keys %switch_db) { # sort to always have the same order
901      $checksum_data .= join ':',
902         $switch_db{$sw_name}->{'description'},
903         $switch_db{$sw_name}->{'model'},
904         $switch_db{$sw_name}->{'hostname'},
905         "\n";
906      }
907
908   return sha512_base64($checksum_data);
909   }
910
911#---------------------------------------------------------------
912sub update_switchdb {
913   my %args = (
914      verbose => 0,
915      @_);
916
917   init_switch_names('yes');    #nomme les switchs
918   print "\n";
919
920   my %where = ();
921   my %db_switch_output_port = ();
922   my %db_switch_ip_hostnamefq = ();
923
924   DETECT_ALL_ROUTER:
925   for my $one_router ( get_list_main_router(get_list_network()) ) {
926      print "Info: router loop $one_router\n" if $args{'verbose'};
927      my %resol_arp = resolve_ip_arp_host($one_router, q{*}, q{low}); # resolution arp
928
929      next DETECT_ALL_ROUTER if $resol_arp{'mac_address'} eq 'unknow';
930      print "VERBOSE_1: Router detected $resol_arp{'ipv4_address'} - $resol_arp{'mac_address'}\n" if $args{'verbose'};
931
932      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{'interface'});
933      my $vlan_id   = get_current_vlan_id($vlan_name);
934      $where{$resol_arp{'ipv4_address'}} = find_all_switch_port($resol_arp{'mac_address'}, $vlan_id); # retrouve les emplacements des routeurs
935      }
936
937   ALL_ROUTER_IP_ADDRESS:
938   for my $ip_router (Net::Netmask::sort_by_ip_address(keys %where)) { # '194.254.66.254')) {
939
940      next ALL_ROUTER_IP_ADDRESS if not exists $where{$ip_router}; # /a priori/ idiot car ne sers à rien...
941
942      ALL_SWITCH_CONNECTED:
943      for my $switch_detected ( keys %{$where{$ip_router}} ) {
944
945         my $switch = $where{$ip_router}->{$switch_detected};
946
947         next ALL_SWITCH_CONNECTED if $switch->{'port_id'} eq '0';
948
949         $db_switch_output_port{$switch->{'hostname'}} = $switch->{'port_hr'};
950         print "VERBOSE_2: output port $switch->{'hostname'} : $switch->{'port_hr'}\n" if $args{'verbose'};
951         }
952      }
953
954   my %db_switch_link_with = ();
955
956   my @list_all_switch = ();
957   my @list_switch_ipv4 = ();
958   for my $sw (@SWITCH_LIST) {
959      push @list_all_switch, $sw->{'hostname'};
960      }
961
962   my $timestamp = time;
963
964   ALL_SWITCH:
965   for my $one_switch (@list_all_switch) {
966      my %resol_arp = resolve_ip_arp_host($one_switch, q{*}, q{low}); # arp resolution
967      if (exists $SWITCH_DB{$one_switch}{'fake-ip'}) {
968         my $fake_ip = $SWITCH_DB{$one_switch}{'fake-ip'};
969         fast_ping($fake_ip);
970         print "WARNING: fake ip on switch $one_switch -> $fake_ip / $resol_arp{'ipv4_address'}\n" if $args{'verbose'};
971         my %resol_arp_alt = resolve_ip_arp_host($fake_ip, q{*}, q{low}); # arp resolution
972         if ($resol_arp_alt{'mac_address'} ne 'unknow') {
973            $resol_arp{'mac_address'}   = $resol_arp_alt{'mac_address'};
974            $resol_arp{'interface'}     = $resol_arp_alt{'interface'};
975            $resol_arp{'ipv4_address'} .= '*';
976            # Force a MAC trace on switch
977            system "arping -c 1 -w 1 -rR -i $resol_arp_alt{'interface'} $fake_ip > /dev/null 2>&1";
978            }
979         }
980      print "Info: switch loop $one_switch\n" if $args{'verbose'};
981      next ALL_SWITCH if $resol_arp{'mac_address'} eq 'unknow';
982
983      push @list_switch_ipv4, $resol_arp{'ipv4_address'};
984
985      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{'interface'});
986      my $vlan_id   = get_current_vlan_id($vlan_name);
987      $where{$resol_arp{'ipv4_address'}} = find_all_switch_port($resol_arp{'mac_address'}, $vlan_id); # find port on all switch
988
989      if ($args{'verbose'}) {
990         print "VERBOSE_3: $one_switch $resol_arp{'ipv4_address'} $resol_arp{'mac_address'}\n";
991         print "VERBOSE_3: $one_switch --- ",
992            join(' + ', keys %{$where{$resol_arp{'ipv4_address'}}}),
993            "\n";
994         }
995
996      $db_switch_ip_hostnamefq{$resol_arp{'ipv4_address'}} = $resol_arp{'hostname_fq'};
997      print "VERBOSE_4: db_switch_ip_hostnamefq $resol_arp{'ipv4_address'} -> $resol_arp{'hostname_fq'}\n" if $args{'verbose'};
998
999      $SWITCH_DB{$one_switch}->{'ipv4_address'} = $resol_arp{'ipv4_address'};
1000      $SWITCH_DB{$one_switch}->{'mac_address'}  = $resol_arp{'mac_address'};
1001      $SWITCH_DB{$one_switch}->{'timestamp'}    = $timestamp;
1002      $SWITCH_DB{$one_switch}->{'network'}      = $vlan_name;
1003      }
1004
1005   ALL_SWITCH_IP_ADDRESS:
1006   for my $ip (@list_switch_ipv4) {
1007#   for my $ip (Net::Netmask::sort_by_ip_address(@list_switch_ipv4)) {
1008
1009      print "VERBOSE_5: loop on $db_switch_ip_hostnamefq{$ip}\n" if $args{'verbose'};
1010
1011      next ALL_SWITCH_IP_ADDRESS if not exists $where{$ip};
1012#      next ALL_SWITCH_IP_ADDRESS if not exists $SWITCH_PORT_COUNT{ $db_switch_ip_hostnamefq{$ip} };
1013
1014      DETECTED_SWITCH:
1015      for my $switch_detected ( keys %{$where{$ip}} ) {
1016
1017         my $switch = $where{$ip}->{$switch_detected};
1018         print "VERBOSE_6: $db_switch_ip_hostnamefq{$ip} -> $switch->{'hostname'} : $switch->{'port_hr'}\n" if $args{'verbose'};
1019
1020         next if $switch->{'port_id'}  eq '0';
1021         next if $switch->{'port_hr'}  eq $db_switch_output_port{$switch->{'hostname'}};
1022         next if $switch->{'hostname'} eq $db_switch_ip_hostnamefq{$ip}; # $computerdb->{$ip}{'hostname'};
1023
1024         $db_switch_link_with{ $db_switch_ip_hostnamefq{$ip} } ||= {};
1025         $db_switch_link_with{ $db_switch_ip_hostnamefq{$ip} }->{ $switch->{'hostname'} } = $switch->{'port_hr'};
1026         print "VERBOSE_7: +++++\n" if $args{'verbose'};
1027         }
1028
1029      }
1030
1031   my %db_switch_connected_on_port = ();
1032   my $maybe_more_than_one_switch_connected = 'yes';
1033   my $cloop = 0;
1034
1035   while ($maybe_more_than_one_switch_connected eq 'yes' and $cloop < 100) {
1036      $cloop++;
1037      print "VERBOSE_9: cloop reduction step: $cloop\n" if $args{'verbose'};
1038      for my $sw (keys %db_switch_link_with) {
1039         for my $connect (keys %{$db_switch_link_with{$sw}}) {
1040
1041            my $port_hr = $db_switch_link_with{$sw}->{$connect};
1042
1043            $db_switch_connected_on_port{"$connect$SEP_SWITCH_PORT$port_hr"} ||= {};
1044            $db_switch_connected_on_port{"$connect$SEP_SWITCH_PORT$port_hr"}->{$sw}++; # Just to define the key
1045            }
1046         }
1047
1048      $maybe_more_than_one_switch_connected  = 'no';
1049
1050      SWITCH_AND_PORT:
1051      for my $swport (keys %db_switch_connected_on_port) {
1052
1053         next if keys %{$db_switch_connected_on_port{$swport}} == 1;
1054
1055         $maybe_more_than_one_switch_connected = 'yes';
1056
1057         my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
1058         my @sw_on_same_port = keys %{$db_switch_connected_on_port{$swport}};
1059         print "VERBOSE_10: $swport -- ".$#sw_on_same_port." -- @sw_on_same_port\n" if $args{'verbose'};
1060
1061         CONNECTED:
1062         for my $sw_connected (@sw_on_same_port) {
1063
1064            next CONNECTED if not keys %{$db_switch_link_with{$sw_connected}} == 1;
1065
1066            $db_switch_connected_on_port{$swport} = {$sw_connected => 1};
1067
1068            for my $other_sw (@sw_on_same_port) {
1069               next if $other_sw eq $sw_connected;
1070
1071               delete $db_switch_link_with{$other_sw}->{$sw_connect};
1072               }
1073
1074            # We can not do better for this switch for this loop
1075            next SWITCH_AND_PORT;
1076            }
1077         }
1078      }
1079
1080   my %db_switch_parent =();
1081
1082   for my $sw (keys %db_switch_link_with) {
1083      for my $connect (keys %{$db_switch_link_with{$sw}}) {
1084
1085         my $port_hr = $db_switch_link_with{$sw}->{$connect};
1086
1087         $db_switch_connected_on_port{"$connect$SEP_SWITCH_PORT$port_hr"} ||= {};
1088         $db_switch_connected_on_port{"$connect$SEP_SWITCH_PORT$port_hr"}->{$sw} = $port_hr;
1089
1090         $db_switch_parent{$sw} = {switch => $connect, port_hr => $port_hr};
1091         }
1092      }
1093
1094   print "Switch output port and parent port connection\n";
1095   print "---------------------------------------------\n";
1096   for my $sw (sort keys %db_switch_output_port) {
1097      if (exists $db_switch_parent{$sw}) {
1098         printf "%-28s  %2s  +-->  %2s  %-25s\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{'port_hr'}, $db_switch_parent{$sw}->{'switch'};
1099         }
1100      else {
1101         printf "%-28s  %2s  +-->  router\n", $sw, $db_switch_output_port{$sw};
1102         }
1103      }
1104   print "\n";
1105
1106   print "Switch parent and children port inter-connection\n";
1107   print "------------------------------------------------\n";
1108   for my $swport (sort keys %db_switch_connected_on_port) {
1109      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
1110      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
1111         if (exists $db_switch_output_port{$sw}) {
1112            printf "%-28s  %2s  <--+  %2s  %-25s\n", $sw_connect, $port_connect, $db_switch_output_port{$sw}, $sw;
1113            }
1114         else {
1115            printf "%-28s  %2s  <--+      %-25s\n", $sw_connect, $port_connect, $sw;
1116            }
1117         }
1118      }
1119
1120   my $switch_connection = {
1121      output_port       => \%db_switch_output_port,
1122      parent            => \%db_switch_parent,
1123      connected_on_port => \%db_switch_connected_on_port,
1124      link_with         => \%db_switch_link_with,
1125      switch_db         => \%SWITCH_DB,
1126      timestamp         => $timestamp,
1127      checksum          => get_switchdb_checksum(%SWITCH_DB),
1128      };
1129
1130   YAML::Syck::DumpFile("$KLASK_SW_FILE", $switch_connection);
1131   return;
1132   }
1133
1134################################################################
1135# command
1136################################################################
1137
1138#---------------------------------------------------------------
1139sub cmd_help {
1140
1141print <<'END';
1142klask - port and search manager for switches, map management
1143
1144 klask version
1145 klask help
1146
1147 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
1148 klask exportdb [--format|-f txt|html]
1149 klask removedb IP* computer*
1150 klask cleandb  [--verbose|-v] --day number_of_day --repair-dns
1151
1152 klask updatesw [--verbose|-v]
1153 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY]
1154
1155 klask searchdb [--kind|-k host|mac] computer [mac-address]
1156 klask search   computer
1157 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr
1158
1159 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
1160
1161 klask bad-vlan-id [--day|-d days_before_alert] [--format|-f txt|html]
1162
1163 klask enable  [--verbose|-v] switch port
1164 klask disable [--verbose|-v] switch port
1165 klask status  [--verbose|-v] switch port
1166
1167 klask poe-enable  [--verbose|-v] switch port
1168 klask poe-disable [--verbose|-v] switch port
1169 klask poe-status  [--verbose|-v] switch port
1170
1171 klask vlan-getname switch vlan-id
1172 klask vlan-list switch
1173END
1174   return;
1175   }
1176
1177#---------------------------------------------------------------
1178sub cmd_version {
1179
1180print <<'END';
1181klask - port and search manager for switches, map management
1182Copyright (C) 2005-2017 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>
1183License GPL version 2 or later and Perl equivalent
1184END
1185
1186   print "Version $VERSION\n\n";
1187   print ' $Id: klask 344 2017-11-01 06:24:34Z g7moreau $'."\n";
1188   return;
1189   }
1190
1191#---------------------------------------------------------------
1192sub cmd_search {
1193   my @computer = @_;
1194
1195   init_switch_names();    #nomme les switchs
1196   fast_ping(@computer);
1197
1198   LOOP_ON_COMPUTER:
1199   for my $clientname (@computer) {
1200      my %resol_arp = resolve_ip_arp_host($clientname);          #resolution arp
1201      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{'interface'});
1202      my $vlan_id   = get_current_vlan_id($vlan_name);
1203      my %where     = find_switch_port($resol_arp{'mac_address'}, '', $vlan_id); #retrouve l'emplacement
1204
1205      next LOOP_ON_COMPUTER if $where{'switch_description'} eq 'unknow' or $resol_arp{'hostname_fq'} eq 'unknow' or $resol_arp{'mac_address'} eq 'unknow';
1206
1207      printf '%-22s %2s %-30s %-15s %18s',
1208         $where{'switch_hostname'},
1209         $where{'switch_port_hr'},
1210         $resol_arp{'hostname_fq'},
1211         $resol_arp{'ipv4_address'},
1212         $resol_arp{'mac_address'}."\n";
1213      }
1214   return;
1215   }
1216
1217#---------------------------------------------------------------
1218sub cmd_searchdb {
1219   my @ARGV  = @_;
1220
1221   my $kind;
1222
1223   GetOptions(
1224      'kind=s'   => \$kind,
1225      );
1226
1227   my %possible_search = (
1228      host  => \&cmd_searchdb_host,
1229      mac   => \&cmd_searchdb_mac,
1230      );
1231
1232   $kind = 'host' if not defined $possible_search{$kind};
1233
1234   $possible_search{$kind}->(@ARGV);
1235   return;
1236   }
1237
1238
1239#---------------------------------------------------------------
1240sub cmd_searchdb_host {
1241   my @computer = @_;
1242
1243   fast_ping(@computer);
1244   my $computerdb = computerdb_load();
1245
1246   LOOP_ON_COMPUTER:
1247   for my $clientname (@computer) {
1248      my %resol_arp = resolve_ip_arp_host($clientname);      #resolution arp
1249      my $ip = $resol_arp{'ipv4_address'};
1250
1251      next LOOP_ON_COMPUTER unless exists $computerdb->{$ip};
1252
1253      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{'timestamp'};
1254      $year += 1900;
1255      $mon++;
1256      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1257
1258      printf "%-22s %2s %-30s %-15s %-18s %s\n",
1259         $computerdb->{$ip}{'switch_hostname'},
1260         $computerdb->{$ip}{'switch_port_hr'},
1261         $computerdb->{$ip}{'hostname_fq'},
1262         $ip,
1263         $computerdb->{$ip}{'mac_address'},
1264         $date;
1265      }
1266   return;
1267   }
1268
1269#---------------------------------------------------------------
1270sub cmd_searchdb_mac {
1271   my @mac = map { normalize_mac_address($_) } @_;
1272
1273   my $computerdb = computerdb_load();
1274
1275   LOOP_ON_MAC:
1276   for my $mac (@mac) {
1277      LOOP_ON_COMPUTER:
1278      for my $ip (keys %{$computerdb}) {
1279         next LOOP_ON_COMPUTER if $mac ne $computerdb->{$ip}{'mac_address'};
1280
1281         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{'timestamp'};
1282         $year += 1900;
1283         $mon++;
1284         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1285
1286         printf "%-22s %2s %-30s %-15s %-18s %s\n",
1287            $computerdb->{$ip}{'switch_hostname'},
1288            $computerdb->{$ip}{'switch_port_hr'},
1289            $computerdb->{$ip}{'hostname_fq'},
1290            $ip,
1291            $computerdb->{$ip}{'mac_address'},
1292            $date;
1293         #next LOOP_ON_MAC;
1294         }
1295
1296      }
1297   return;
1298   }
1299
1300#---------------------------------------------------------------
1301sub cmd_updatedb {
1302   @ARGV = @_;
1303
1304   my ($verbose, $verb_description, $check_hostname, $check_location, $no_rebuildsw);
1305
1306   GetOptions(
1307      'verbose|v'          => \$verbose,
1308      'verb-description|d' => \$verb_description,
1309      'chk-hostname|h'     => \$check_hostname,
1310      'chk-location|l'     => \$check_location,
1311      'no-rebuildsw|R'     => \$no_rebuildsw,
1312      );
1313
1314   my @network = @ARGV;
1315      @network = get_list_network() if not @network;
1316
1317   test_switchdb_environnement();
1318
1319   my $computerdb = {};
1320      $computerdb = computerdb_load() if -e "$KLASK_DB_FILE";
1321   my $timestamp = time;
1322
1323   my %computer_not_detected = ();
1324   my $timestamp_last_week = $timestamp - (3600 * 24 * 7);
1325
1326   my $number_of_computer = get_list_ip(@network); # + 1;
1327   my $size_of_database   = keys %{$computerdb};
1328      $size_of_database   = 1 if $size_of_database == 0;
1329   my $i = 0;
1330   my $detected_computer = 0;
1331
1332   init_switch_names('yes', $verb_description, $check_hostname, $check_location);    #nomme les switchs
1333
1334   {
1335   my $switch_checksum = get_switchdb_checksum(%SWITCH_DB);
1336   # Remplis le champs portignore des ports d'inter-connection pour chaque switch
1337   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
1338   if ($switch_checksum ne $switch_connection->{'checksum'}) { # Verify checksum
1339      if ($no_rebuildsw) {
1340         print "WARNING: switch database is outdate, please rebuild if with updatesw command\n";
1341         }
1342      else {
1343         print "WARNING: switch database is going to be rebuilt\n";
1344         update_switchdb(verbose => $verbose)
1345         }
1346      }
1347
1348   my %db_switch_output_port       = %{$switch_connection->{'output_port'}};
1349   my %db_switch_connected_on_port = %{$switch_connection->{'connected_on_port'}};
1350   my %db_switch_chained_port = ();
1351   for my $swport (keys %db_switch_connected_on_port) {
1352      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
1353      $db_switch_chained_port{$sw_connect} .= "$port_connect:";
1354      }
1355   for my $sw (@SWITCH_LIST) {
1356      push @{$sw->{'portignore'}}, $db_switch_output_port{$sw->{'hostname'}}  if exists $db_switch_output_port{$sw->{'hostname'}};
1357      if ( exists $db_switch_chained_port{$sw->{'hostname'}} ) {
1358         chop $db_switch_chained_port{$sw->{'hostname'}};
1359         push @{$sw->{'portignore'}}, split m/ : /xms, $db_switch_chained_port{$sw->{'hostname'}};
1360         }
1361#      print "$sw->{'hostname'} ++ @{$sw->{'portignore'}}\n";
1362      }
1363   }
1364
1365   my %router_mac_ip = ();
1366   DETECT_ALL_ROUTER:
1367#   for my $one_router ('194.254.66.254') {
1368   for my $one_router ( get_list_main_router(@network) ) {
1369      my %resol_arp = resolve_ip_arp_host($one_router);
1370      $router_mac_ip{ $resol_arp{'mac_address'} } = $resol_arp{'ipv4_address'};
1371      }
1372
1373   ALL_NETWORK:
1374   for my $current_net (@network) {
1375
1376      my @computer = get_list_ip($current_net);
1377      my $current_interface = get_current_interface($current_net);
1378
1379      fast_ping(@computer) if get_current_scan_mode($current_net) eq 'active';
1380
1381      LOOP_ON_COMPUTER:
1382      for my $one_computer (@computer) {
1383         $i++;
1384
1385         my $total_percent = int (($i*100)/$number_of_computer);
1386
1387         my $localtime = time - $timestamp;
1388         my ($sec,$min) = localtime $localtime;
1389
1390         my $time_elapse = 0;
1391            $time_elapse = $localtime * ( 100 - $total_percent) / $total_percent if $total_percent != 0;
1392         my ($sec_elapse,$min_elapse) = localtime $time_elapse;
1393
1394         printf "\rComputer scanned: %4i/%i (%2i%%)",  $i,                 $number_of_computer, $total_percent;
1395         printf ', detected: %4i/%i (%2i%%)', $detected_computer, $size_of_database,   int(($detected_computer*100)/$size_of_database);
1396         printf ' [Time: %02i:%02i / %02i:%02i]', int($localtime/60), $localtime % 60, int($time_elapse/60), $time_elapse % 60;
1397         printf ' %-8s %-14s', $current_interface, $one_computer;
1398
1399         my $already_exist = exists $computerdb->{$one_computer} ? 'yes' : 'no';
1400         my %resol_arp = resolve_ip_arp_host($one_computer, $current_interface, 'fast', $already_exist);
1401
1402         # do not search on router connection (why ?)
1403         if ( exists $router_mac_ip{$resol_arp{'mac_address'}}) {
1404            $computer_not_detected{$one_computer} = $current_net;
1405            next LOOP_ON_COMPUTER;
1406            }
1407
1408         # do not search on switch inter-connection
1409         if (exists $SWITCH_LEVEL{$resol_arp{'hostname_fq'}}) {
1410            $computer_not_detected{$one_computer} = $current_net;
1411            next LOOP_ON_COMPUTER;
1412            }
1413
1414         my $switch_proposal = q{};
1415         if (exists $computerdb->{$resol_arp{'ipv4_address'}} and exists $computerdb->{$resol_arp{'ipv4_address'}}{'switch_hostname'}) {
1416            $switch_proposal = $computerdb->{$resol_arp{'ipv4_address'}}{'switch_hostname'};
1417            }
1418
1419         # do not have a mac address
1420         if ($resol_arp{'mac_address'} eq 'unknow' or (exists $resol_arp{'timestamps'} and $resol_arp{'timestamps'} < ($timestamp - 3 * 3600))) {
1421            $computer_not_detected{$one_computer} = $current_net;
1422            next LOOP_ON_COMPUTER;
1423            }
1424
1425         my $vlan_name = get_current_vlan_name_for_interface($resol_arp{'interface'});
1426         my $vlan_id   = get_current_vlan_id($vlan_name);
1427         my %where = find_switch_port($resol_arp{'mac_address'}, $switch_proposal, $vlan_id);
1428
1429         #192.168.24.156:
1430         #  arp: 00:0B:DB:D5:F6:65
1431         #  hostname: pcroyon.hmg.priv
1432         #  port: 5
1433         #  switch: sw-batH-legi:hp2524
1434         #  timestamp: 1164355525
1435
1436         # do not have a mac address
1437#         if ($resol_arp{'mac_address'} eq 'unknow') {
1438#            $computer_not_detected{$one_computer} = $current_interface;
1439#            next LOOP_ON_COMPUTER;
1440#            }
1441
1442         # detected on a switch
1443         if ($where{'switch_description'} ne 'unknow') {
1444            $detected_computer++;
1445            $computerdb->{$resol_arp{'ipv4_address'}} = {
1446               hostname_fq        => $resol_arp{'hostname_fq'},
1447               mac_address        => $resol_arp{'mac_address'},
1448               switch_hostname    => $where{'switch_hostname'},
1449               switch_description => $where{'switch_description'},
1450               switch_port_id     => $where{'switch_port_id'},
1451               switch_port_hr     => $where{'switch_port_hr'},
1452               timestamp          => $timestamp,
1453               network            => $current_net,
1454               };
1455            next LOOP_ON_COMPUTER;
1456            }
1457
1458         # new in the database but where it is ?
1459         if (not exists $computerdb->{$resol_arp{'ipv4_address'}}) {
1460            $detected_computer++;
1461            $computerdb->{$resol_arp{'ipv4_address'}} = {
1462               hostname_fq        => $resol_arp{'hostname_fq'},
1463               mac_address        => $resol_arp{'mac_address'},
1464               switch_hostname    => $where{'switch_hostname'},
1465               switch_description => $where{'switch_description'},
1466               switch_port_id     => $where{'switch_port_id'},
1467               switch_port_hr     => $where{'switch_port_hr'},
1468               timestamp          => $resol_arp{'timestamp'},
1469               network            => $current_net,
1470               };
1471            }
1472
1473         # mise a jour du nom de la machine si modification dans le dns
1474         $computerdb->{$resol_arp{'ipv4_address'}}{'hostname_fq'} = $resol_arp{'hostname_fq'};
1475
1476         # mise à jour de la date de détection si détection plus récente par arpwatch
1477         $computerdb->{$resol_arp{'ipv4_address'}}{'timestamp'}   = $resol_arp{'timestamp'} if exists $resol_arp{'timestamp'} and $computerdb->{$resol_arp{'ipv4_address'}}{'timestamp'} < $resol_arp{'timestamp'};
1478
1479         # relance un arping sur la machine si celle-ci n'a pas été détectée depuis plus d'une semaine
1480#         push @computer_not_detected, $resol_arp{'ipv4_address'} if $computerdb->{$resol_arp{'ipv4_address'}}{'timestamp'} < $timestamp_last_week;
1481         $computer_not_detected{$resol_arp{'ipv4_address'}} = $current_net if $computerdb->{$resol_arp{'ipv4_address'}}{'timestamp'} < $timestamp_last_week;
1482
1483         }
1484      }
1485
1486   # final end of line at the end of the loop
1487   printf "\n";
1488
1489   my $dirdb = $KLASK_DB_FILE;
1490      $dirdb =~ s{ / [^/]* $}{}xms;
1491   mkdir "$dirdb", 0755 unless -d "$dirdb";
1492   YAML::Syck::DumpFile("$KLASK_DB_FILE", $computerdb);
1493
1494   for my $one_computer (keys %computer_not_detected) {
1495      my $current_net = $computer_not_detected{$one_computer};
1496      my $current_interface = get_current_interface($current_net);
1497      system "arping -c 1 -w 1 -rR -i $current_interface $one_computer > /dev/null 2>&1" if get_current_scan_mode($current_net) eq 'active';
1498      }
1499   return;
1500   }
1501
1502#---------------------------------------------------------------
1503sub cmd_removedb {
1504   my @computer = @_;
1505
1506   test_maindb_environnement();
1507
1508   my $computerdb = computerdb_load();
1509
1510   LOOP_ON_COMPUTER:
1511   for my $one_computer (@computer) {
1512
1513      if ( $one_computer =~ m/^ $RE_IPv4_ADDRESS $/xms
1514            and exists $computerdb->{$one_computer} ) {
1515         delete $computerdb->{$one_computer};
1516         next;
1517         }
1518
1519      my %resol_arp = resolve_ip_arp_host($one_computer);
1520
1521      delete $computerdb->{$resol_arp{'ipv4_address'}} if exists $computerdb->{$resol_arp{'ipv4_address'}};
1522      }
1523
1524   my $dirdb = $KLASK_DB_FILE;
1525      $dirdb =~ s{ / [^/]* $}{}xms;
1526   mkdir "$dirdb", 0755 unless -d "$dirdb";
1527   YAML::Syck::DumpFile("$KLASK_DB_FILE", $computerdb);
1528   return;
1529   }
1530
1531#---------------------------------------------------------------
1532sub cmd_cleandb {
1533   my @ARGV  = @_;
1534
1535   my $days_to_clean = 15;
1536   my $repairdns;
1537   my $verbose;
1538   my $database_has_changed;
1539
1540   GetOptions(
1541      'day|d=i'   => \$days_to_clean,
1542      'verbose|v' => \$verbose,
1543      'repair-dns|r' => \$repairdns,
1544      );
1545
1546   my @vlan_name = get_list_network();
1547
1548   my $computerdb = computerdb_load();
1549   my $timestamp = time;
1550
1551   my $timestamp_barrier = 3600 * 24 * $days_to_clean;
1552   my $timestamp_3month  = 3600 * 24 * 90;
1553
1554   my %mactimedb = ();
1555   ALL_VLAN:
1556   for my $vlan (shuffle @vlan_name) {
1557
1558      my @ip_list   = shuffle get_list_ip($vlan);
1559
1560      LOOP_ON_IP_ADDRESS:
1561      for my $ip (@ip_list) {
1562
1563         next LOOP_ON_IP_ADDRESS if
1564            not exists $computerdb->{$ip};
1565
1566            #&& $computerdb->{$ip}{'timestamp'} > $timestamp_barrier;
1567         my $ip_timestamp   = $computerdb->{$ip}{'timestamp'};
1568         my $ip_mac         = $computerdb->{$ip}{'mac_address'};
1569         my $ip_hostname_fq = $computerdb->{$ip}{'hostname_fq'};
1570
1571         $mactimedb{$ip_mac} ||= {
1572            ip          => $ip,
1573            timestamp   => $ip_timestamp,
1574            vlan        => $vlan,
1575            hostname_fq => $ip_hostname_fq,
1576            };
1577
1578         if (
1579            ( $mactimedb{$ip_mac}->{'timestamp'} - $ip_timestamp > $timestamp_barrier
1580               or (
1581                  $mactimedb{$ip_mac}->{'timestamp'} > $ip_timestamp
1582                  and $timestamp - $mactimedb{$ip_mac}->{'timestamp'} > $timestamp_3month
1583                  )
1584            )
1585            and (
1586               not $mactimedb{$ip_mac}->{'hostname_fq'} =~ m/$RE_FLOAT_HOSTNAME/
1587               or $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/
1588               )) {
1589            print "remove ip $ip\n" if $verbose;
1590            delete $computerdb->{$ip};
1591            $database_has_changed++;
1592            }
1593
1594         elsif (
1595            ( $ip_timestamp - $mactimedb{$ip_mac}->{'timestamp'} > $timestamp_barrier
1596               or (
1597                  $ip_timestamp > $mactimedb{$ip_mac}->{'timestamp'}
1598                  and $timestamp - $ip_timestamp > $timestamp_3month
1599                  )
1600            )
1601            and (
1602               not $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/
1603               or $mactimedb{$ip_mac}->{'hostname_fq'} =~ m/$RE_FLOAT_HOSTNAME/
1604               )) {
1605            print "remove ip ".$mactimedb{$ip_mac}->{'ip'}."\n" if $verbose;
1606            delete $computerdb->{$mactimedb{$ip_mac}->{'ip'}};
1607            $database_has_changed++;
1608            }
1609
1610         if ( $ip_timestamp > $mactimedb{$ip_mac}->{'timestamp'}) {
1611            $mactimedb{$ip_mac} = {
1612               ip          => $ip,
1613               timestamp   => $ip_timestamp,
1614               vlan        => $vlan,
1615               hostname_fq => $ip_hostname_fq,
1616               };
1617            }
1618         }
1619      }
1620
1621   if ($repairdns) { # Search and update unkown computer in reverse DNS
1622      LOOP_ON_IP_ADDRESS:
1623      for my $ip (keys %{$computerdb}) {
1624         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} ne 'unknow';
1625
1626         my $packed_ip = scalar gethostbyname($ip);
1627         next LOOP_ON_IP_ADDRESS if not defined $packed_ip;
1628
1629         my $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET);
1630         next LOOP_ON_IP_ADDRESS if not defined $hostname_fq;
1631
1632         $computerdb->{$ip}{'hostname_fq'} = $hostname_fq;
1633         $database_has_changed++;
1634         }
1635      }
1636
1637   if ( $database_has_changed ) {
1638      my $dirdb = $KLASK_DB_FILE;
1639         $dirdb =~ s{ / [^/]* $}{}xms;
1640      mkdir "$dirdb", 0755 unless -d "$dirdb";
1641      YAML::Syck::DumpFile("$KLASK_DB_FILE", $computerdb);
1642      }
1643   return;
1644   }
1645
1646#---------------------------------------------------------------
1647sub cmd_exportdb {
1648   @ARGV = @_;
1649
1650   my $format = 'txt';
1651
1652   GetOptions(
1653      'format|f=s'  => \$format,
1654      );
1655
1656   my %possible_format = (
1657      txt  => \&cmd_exportdb_txt,
1658      html => \&cmd_exportdb_html,
1659      );
1660
1661   $format = 'txt' if not defined $possible_format{$format};
1662
1663   $possible_format{$format}->(@ARGV);
1664   return;
1665   }
1666
1667#---------------------------------------------------------------
1668sub cmd_exportdb_txt {
1669   test_maindb_environnement();
1670
1671   my $computerdb = computerdb_load();
1672
1673   my $tb_computer = Text::Table->new(
1674      {align => 'left',   align_title => 'left',   title => 'Switch'},
1675      {align => 'right',  align_title => 'right',  title => 'Port'},
1676      {align => 'center', align_title => 'center', title => 'Link'},
1677      {align => 'left',   align_title => 'left',   title => 'Hostname-FQ'},
1678      {align => 'left',   align_title => 'left',   title => 'IPv4-Address'},
1679      {align => 'left',   align_title => 'left',   title => 'MAC-Address'},
1680      {align => 'left',   align_title => 'left',   title => 'Date'},
1681      {align => 'left',   align_title => 'left',   title => 'VLAN'},
1682      );
1683
1684   LOOP_ON_IP_ADDRESS:
1685   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
1686
1687      # to be improve in the future
1688      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
1689
1690      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{'timestamp'};
1691      $year += 1900;
1692      $mon++;
1693      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1694
1695      my $vlan = '';
1696      $vlan = $computerdb->{$ip}{'network'}.'('.get_current_vlan_id($computerdb->{$ip}{'network'}).')' if $computerdb->{$ip}{'network'};
1697
1698      my $arrow ='<---';
1699         $arrow ='<===' if $computerdb->{$ip}{'switch_port_hr'} =~ m/^(Trk|Br|Po)/;
1700
1701      $tb_computer->add(
1702         $computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'},
1703         $computerdb->{$ip}{'switch_port_hr'},
1704         $arrow,
1705         $computerdb->{$ip}{'hostname_fq'},
1706         $ip,
1707         $computerdb->{$ip}{'mac_address'},
1708         $date,
1709         $vlan,
1710         );
1711      }
1712
1713   print $tb_computer->title();
1714   print $tb_computer->rule('-');
1715   print $tb_computer->body();
1716
1717   return;
1718   }
1719
1720#---------------------------------------------------------------
1721sub cmd_exportdb_html {
1722   test_maindb_environnement();
1723
1724   my $computerdb = computerdb_load();
1725
1726#<link rel="stylesheet" type="text/css" href="style-klask.css" />
1727#<script src="sorttable-klask.js"></script>
1728
1729   print <<'END_HTML';
1730<table class="sortable" summary="Klask Host Database">
1731 <caption>Klask Host Database</caption>
1732 <thead>
1733  <tr>
1734   <th scope="col" class="klask-header-left">Switch&nbsp;&#8645;</th>
1735   <th scope="col" class="sorttable_nosort">Port</th>
1736   <th scope="col" class="sorttable_nosort">Link</th>
1737   <th scope="col" class="sorttable_alpha">Hostname-FQ&nbsp;&#8645;</th>
1738   <th scope="col" class="hklask-ipv4">IPv4-Address&nbsp;&#8645;</th>
1739   <th scope="col" class="sorttable_alpha">MAC-Address&nbsp;&#8645;</th>
1740   <th scope="col" class="sorttable_alpha">VLAN&nbsp;&#8645;</th>
1741   <th scope="col" class="klask-header-right">Date&nbsp;&#8645;</th>
1742  </tr>
1743 </thead>
1744 <tfoot>
1745  <tr>
1746   <th scope="col" class="klask-footer-left">Switch</th>
1747   <th scope="col" class="fklask-port">Port</th>
1748   <th scope="col" class="fklask-link">Link</th>
1749   <th scope="col" class="fklask-hostname">Hostname-FQ</th>
1750   <th scope="col" class="fklask-ipv4">IPv4-Address</th>
1751   <th scope="col" class="fklask-mac">MAC-Address</th>
1752   <th scope="col" class="fklask-vlan">VLAN</th>
1753   <th scope="col" class="klask-footer-right">Date</th>
1754  </tr>
1755 </tfoot>
1756 <tbody>
1757END_HTML
1758
1759   my %mac_count = ();
1760   LOOP_ON_IP_ADDRESS:
1761   for my $ip (keys %{$computerdb}) {
1762
1763      # to be improve in the future
1764      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
1765
1766      $mac_count{$computerdb->{$ip}{'mac_address'}}++;
1767      }
1768
1769   my $typerow = 'even';
1770
1771   LOOP_ON_IP_ADDRESS:
1772   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
1773
1774      # to be improve in the future
1775      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
1776
1777      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{'timestamp'};
1778      $year += 1900;
1779      $mon++;
1780      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1781
1782#      $odd_or_even++;
1783#      my $typerow = $odd_or_even % 2 ? 'odd' : 'even';
1784      $typerow = $typerow eq 'even' ? 'odd' : 'even';
1785
1786      #my $arrow ='&#8592;';
1787      #   $arrow ='&#8656;' if $computerdb->{$ip}{'switch_port_hr'} =~ m/^(Trk|Br|Po)/;
1788      my $arrow ='&#10229;';
1789         $arrow ='&#10232;' if $computerdb->{$ip}{'switch_port_hr'} =~ m/^(Trk|Br|Po)/;
1790
1791      my $switch_hostname = $computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'} || 'unkown';
1792      chomp $switch_hostname;
1793      my $switch_hostname_sort = sprintf '%s %06i' ,$switch_hostname, $computerdb->{$ip}{'switch_port_id'}; # Take switch index
1794
1795      my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ip;
1796
1797      my $mac_sort = sprintf '%04i-%s', 9999 - $mac_count{$computerdb->{$ip}{'mac_address'}}, $computerdb->{$ip}{'mac_address'};
1798
1799      $computerdb->{$ip}{'hostname_fq'} = 'unknow' if $computerdb->{$ip}{'hostname_fq'} =~ m/^ \d+ \. \d+ \. \d+ \. \d+ $/xms;
1800      my ( $host_short ) = split m/ \. /xms, $computerdb->{$ip}{'hostname_fq'};
1801
1802      my $vlan = '';
1803      $vlan = $computerdb->{$ip}{'network'}.' ('.get_current_vlan_id($computerdb->{$ip}{'network'}).')' if $computerdb->{$ip}{'network'};
1804
1805      my $parent_port_hr = format_aggregator4html($computerdb->{$ip}{'switch_port_hr'});
1806
1807      fqdn_html_breakable($switch_hostname);
1808      fqdn_html_breakable(my $hostname_fq_html = $computerdb->{$ip}{'hostname_fq'});
1809
1810      print <<"END_HTML";
1811  <tr class="$typerow">
1812   <td sorttable_customkey="$switch_hostname_sort">$switch_hostname</td>
1813   <td class="bklask-port">$parent_port_hr</td>
1814   <td class="bklask-arrow">$arrow</td>
1815   <td sorttable_customkey="$host_short">$hostname_fq_html</td>
1816   <td sorttable_customkey="$ip_sort">$ip</td>
1817   <td sorttable_customkey="$mac_sort">$computerdb->{$ip}{'mac_address'}</td>
1818   <td>$vlan</td>
1819   <td>$date</td>
1820  </tr>
1821END_HTML
1822      }
1823
1824   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
1825
1826   my %db_switch_output_port       = %{$switch_connection->{'output_port'}};
1827   my %db_switch_parent            = %{$switch_connection->{'parent'}};
1828   my %db_switch_connected_on_port = %{$switch_connection->{'connected_on_port'}};
1829   my %db_switch                   = %{$switch_connection->{'switch_db'}};
1830
1831   # Output switch connection
1832   LOOP_ON_OUTPUT_SWITCH:
1833   for my $sw (sort keys %db_switch_output_port) {
1834
1835      my $switch_hostname_sort = sprintf '%s %3s' ,$sw, $db_switch_output_port{$sw};
1836
1837      $typerow = $typerow eq 'even' ? 'odd' : 'even';
1838
1839      #my $arrow ='&#8702;';
1840      #   $arrow ='&#8680;' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
1841      my $arrow ='&#10236;';
1842         $arrow ='&#10238;' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
1843
1844      if (exists $db_switch_parent{$sw}) {
1845         # Link to uplink switch
1846         next LOOP_ON_OUTPUT_SWITCH;
1847
1848         # Do not print anymore
1849         my $mac_address  = $db_switch{$db_switch_parent{$sw}->{'switch'}}->{'mac_address'};
1850         my $ipv4_address = $db_switch{$db_switch_parent{$sw}->{'switch'}}->{'ipv4_address'};
1851         my $timestamp    = $db_switch{$db_switch_parent{$sw}->{'switch'}}->{'timestamp'};
1852
1853         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $timestamp;
1854         $year += 1900;
1855         $mon++;
1856         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1857
1858         my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ [\.\*] /xms, $ipv4_address; # \* for fake-ip
1859
1860         my $mac_sort = sprintf '%04i-%s', 9999, $mac_address;
1861
1862         my ( $host_short ) = sprintf '%s %3s' , split(m/ \. /xms, $db_switch_parent{$sw}->{'switch'}, 1), $db_switch_parent{$sw}->{'port_hr'};
1863
1864         my $vlan = $db_switch{$db_switch_parent{$sw}->{'switch'}}->{'network'};
1865         $vlan .= ' ('.get_current_vlan_id($db_switch{$db_switch_parent{$sw}->{'switch'}}->{'network'}).')' if $db_switch{$db_switch_parent{$sw}->{'switch'}}->{'network'};
1866
1867         my $parent_port_hr = format_aggregator4html($db_switch_output_port{$sw});
1868         my $child_port_hr  = format_aggregator4html($db_switch_parent{$sw}->{'port_hr'});
1869
1870         fqdn_html_breakable($sw);
1871         fqdn_html_breakable(my $sw_child_html = $db_switch_parent{$sw}->{'switch'});
1872
1873         print <<"END_HTML";
1874  <tr class="$typerow">
1875   <td sorttable_customkey="$switch_hostname_sort">$sw</td>
1876   <td class="bklask-port">$parent_port_hr</td>
1877   <td class="bklask-arrow">$arrow $child_port_hr</td>
1878   <td sorttable_customkey="$host_short">$sw_child_html</td>
1879   <td sorttable_customkey="$ip_sort">$ipv4_address</td>
1880   <td sorttable_customkey="$mac_sort">$mac_address</td>
1881   <td>$vlan</td>
1882   <td>$date</td>
1883  </tr>
1884END_HTML
1885         }
1886      else {
1887         # Router
1888         my $parent_port_hr = format_aggregator4html($db_switch_output_port{$sw});
1889
1890         my $host_short = sprintf '%s %3s' ,$sw, $db_switch_output_port{$sw};
1891
1892         my $mac_address = $db_switch{$sw}->{'mac_address'};
1893         my $ipv4_address = $db_switch{$sw}->{'ipv4_address'};
1894         my $timestamp = $db_switch{$sw}->{'timestamp'};
1895
1896         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $timestamp;
1897         $year += 1900;
1898         $mon++;
1899         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
1900
1901         my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ [\.\*] /xms, $ipv4_address; # \* for fake-ip
1902
1903         my $mac_sort = sprintf '%04i-%s', 9999, $mac_address;
1904
1905         my $vlan = $db_switch{$sw}->{'network'};
1906         $vlan .= ' ('.get_current_vlan_id($db_switch{$sw}->{'network'}).')' if $db_switch{$sw}->{'network'};
1907
1908         my $arrow ='&#10235;';
1909            $arrow ='&#10237;' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
1910
1911         fqdn_html_breakable($sw);
1912
1913         print <<"END_HTML";
1914  <tr class="$typerow">
1915   <td sorttable_customkey="router">router</td>
1916   <td class="bklask-port"></td>
1917   <td class="bklask-arrow">$arrow $parent_port_hr</td>
1918   <td sorttable_customkey="$host_short">$sw</td>
1919   <td sorttable_customkey="$ip_sort">$ipv4_address</td>
1920   <td sorttable_customkey="$mac_sort">$mac_address</td>
1921   <td>$vlan</td>
1922   <td>$date</td>
1923  </tr>
1924END_HTML
1925         next LOOP_ON_OUTPUT_SWITCH;
1926         }
1927      }
1928
1929   # Child switch connection : parent <- child
1930   for my $swport (sort keys %db_switch_connected_on_port) {
1931      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
1932      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
1933
1934         my $switch_hostname_sort = sprintf '%s %3s' ,$sw_connect, $port_connect;
1935
1936         my $mac_address = $db_switch{$sw}->{'mac_address'};
1937         my $ipv4_address = $db_switch{$sw}->{'ipv4_address'};
1938         my $timestamp = $db_switch{$sw}->{'timestamp'};
1939
1940         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $timestamp;
1941         $year += 1900;
1942         $mon++;
1943         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year,$mon,$mday,$hour,$min;
1944
1945         my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ [\.\*] /xms, $ipv4_address; # \* for fake-ip
1946
1947         my $mac_sort = sprintf '%04i-%s', 9999, $mac_address;
1948
1949         $typerow = $typerow eq 'even' ? 'odd' : 'even';
1950
1951         #my $arrow ='&#8701;';
1952         #   $arrow ='&#8678;' if $port_connect =~ m/^(Trk|Br|Po)/;
1953         my $arrow ='&#10235;';
1954            $arrow ='&#10237;' if $port_connect =~ m/^(Trk|Br|Po)/;
1955
1956         my $vlan = $db_switch{$sw}->{'network'};
1957         $vlan .= ' ('.get_current_vlan_id($db_switch{$sw}->{'network'}).')' if $db_switch{$sw}->{'network'};
1958
1959         if (exists $db_switch_output_port{$sw}) {
1960
1961            my ( $host_short ) = sprintf '%s %3s' , split( m/\./xms, $sw, 1), $db_switch_output_port{$sw};
1962
1963            my $parent_port_hr = format_aggregator4html($port_connect);
1964            my $child_port_hr  = format_aggregator4html($db_switch_output_port{$sw});
1965
1966            fqdn_html_breakable($sw);
1967            fqdn_html_breakable($sw_connect);
1968
1969            print <<"END_HTML";
1970  <tr class="$typerow">
1971   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
1972   <td class="bklask-port">$parent_port_hr</td>
1973   <td class="bklask-arrow">$arrow $child_port_hr</td>
1974   <td sorttable_customkey="$host_short">$sw</td>
1975   <td sorttable_customkey="$ip_sort">$ipv4_address</td>
1976   <td sorttable_customkey="$mac_sort">$mac_address</td>
1977   <td>$vlan</td>
1978   <td>$date</td>
1979  </tr>
1980END_HTML
1981            }
1982         else {
1983            my $parent_port_hr = format_aggregator4html($port_connect);
1984
1985            fqdn_html_breakable($sw);
1986            fqdn_html_breakable($sw_connect);
1987
1988            print <<"END_HTML";
1989  <tr class="$typerow">
1990   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
1991   <td class="bklask-port">$parent_port_hr</td>
1992   <td class="bklask-arrow">$arrow</td>
1993   <td sorttable_customkey="$sw">$sw</td>
1994   <td sorttable_customkey="">$ipv4_address</td>
1995   <td sorttable_customkey="">$mac_address</td>
1996   <td>$vlan</td>
1997   <td>$date</td>
1998  </tr>
1999END_HTML
2000            }
2001         }
2002      }
2003
2004   print <<'END_HTML';
2005 </tbody>
2006</table>
2007END_HTML
2008   return;
2009   }
2010
2011#---------------------------------------------------------------
2012sub cmd_bad_vlan_id {
2013   @ARGV = @_;
2014
2015   my $days_before_alert = $DEFAULT{'days-before-alert'} || 15;
2016   my $format = 'txt';
2017   my $verbose;
2018
2019   GetOptions(
2020      'day|d=i'      => \$days_before_alert,
2021      'format|f=s'   => \$format,
2022      );
2023
2024   my %possible_format = (
2025      txt  => \&cmd_bad_vlan_id_txt,
2026      html => \&cmd_bad_vlan_id_html,
2027      none => sub {},
2028      );
2029   $format = 'txt' if not defined $possible_format{$format};
2030
2031   test_maindb_environnement();
2032
2033   my $computerdb = computerdb_load();
2034
2035   # create a database with the most recent computer by switch port
2036   my %switchportdb = ();
2037   LOOP_ON_IP_ADDRESS:
2038   for my $ip (keys %{$computerdb}) {
2039      # to be improve in the future
2040      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
2041      next LOOP_ON_IP_ADDRESS if ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}) eq 'unknow';
2042      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'switch_port_id'} eq '0';
2043
2044      my $ip_timestamp   = $computerdb->{$ip}{'timestamp'};
2045      my $ip_mac         = $computerdb->{$ip}{'mac_address'};
2046      my $ip_hostname_fq = $computerdb->{$ip}{'hostname_fq'};
2047
2048      my $swpt = sprintf "%-28s  %2s",
2049         $computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'},
2050         $computerdb->{$ip}{'switch_port_hr'};
2051      $switchportdb{$swpt} ||= {
2052         ip          => $ip,
2053         timestamp   => $ip_timestamp,
2054         vlan        => $computerdb->{$ip}{'network'},
2055         hostname_fq => $ip_hostname_fq,
2056         mac_address => $ip_mac,
2057         };
2058
2059      # if float computer, set date 15 day before warning...
2060      my $ip_timestamp_mod = $ip_timestamp;
2061      my $ip_timestamp_ref = $switchportdb{$swpt}->{'timestamp'};
2062      $ip_timestamp_mod -= $days_before_alert * 24 * 3600 if $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;
2063      $ip_timestamp_ref -= $days_before_alert * 24 * 3600 if $switchportdb{$swpt}->{'hostname_fq'} =~ m/$RE_FLOAT_HOSTNAME/;
2064
2065      if ($ip_timestamp_mod > $ip_timestamp_ref) {
2066         $switchportdb{$swpt} = {
2067            ip          => $ip,
2068            timestamp   => $ip_timestamp,
2069            vlan        => $computerdb->{$ip}{'network'},
2070            hostname_fq => $ip_hostname_fq,
2071            mac_address => $ip_mac,
2072            };
2073         }
2074      }
2075
2076   my @result = ();
2077
2078   LOOP_ON_RECENT_COMPUTER:
2079   for my $swpt (keys %switchportdb) {
2080      next LOOP_ON_RECENT_COMPUTER if $swpt =~ m/^\s*0$/;
2081      next LOOP_ON_RECENT_COMPUTER if $switchportdb{$swpt}->{'hostname_fq'} !~ m/$RE_FLOAT_HOSTNAME/;
2082
2083      my $src_ip = $switchportdb{$swpt}->{'ip'};
2084      my $src_timestamp = 0;
2085      LOOP_ON_IP_ADDRESS:
2086      for my $ip (keys %{$computerdb}) {
2087         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'mac_address'} ne  $switchportdb{$swpt}->{'mac_address'};
2088         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} =~ m/$RE_FLOAT_HOSTNAME/;
2089         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'timestamp'} < $src_timestamp;
2090
2091         $src_ip = $ip;
2092         $src_timestamp = $computerdb->{$ip}{'timestamp'};
2093         }
2094
2095      # keep only if float computer is the most recent
2096      next LOOP_ON_RECENT_COMPUTER if $src_timestamp == 0;
2097      next LOOP_ON_RECENT_COMPUTER if $switchportdb{$swpt}->{'timestamp'} < $src_timestamp;
2098
2099      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $switchportdb{$swpt}->{'timestamp'};
2100      $year += 1900;
2101      $mon++;
2102      my $date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;
2103
2104      ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$src_ip}{'timestamp'};
2105      $year += 1900;
2106      $mon++;
2107      my $src_date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;
2108
2109      my $vlan_id = get_current_vlan_id($computerdb->{$src_ip}{'network'});
2110      my ($switch_hostname, $port_hr) = split /\s+/, $swpt, 2;
2111     
2112      push @result, {
2113         switch      => $switch_hostname,
2114         port_hr     => $port_hr,
2115         vlan_bad    => $switchportdb{$swpt}->{'vlan'},
2116         vlan_good   =>$computerdb->{$src_ip}{'network'},
2117         vlan_id     => $vlan_id,
2118         date_last   => $date,
2119         date_good   => $src_date,
2120         mac_address => $computerdb->{$src_ip}{'mac_address'},
2121         hostname_fq => $computerdb->{$src_ip}{'hostname_fq'},
2122         };
2123      }
2124
2125   $possible_format{$format}->(@result);
2126   }
2127
2128#---------------------------------------------------------------
2129sub cmd_bad_vlan_id_txt {
2130   my @result = @_;
2131
2132   my $tb_bad = Text::Table->new(
2133      {align  => 'left',   align_title => 'left',   title => 'Switch'},
2134      {is_sep => 1,              title => ' ',       body => ' '},
2135      {align  => 'right',  align_title => 'right',  title => 'Port'},
2136      {is_sep => 1,              title => ' ',       body => ' !'},
2137      {align  => 'left',   align_title => 'left',   title => 'VLAN-Bad'},
2138      {is_sep => 1,              title => ' ',       body => ' +-> '},
2139      {align  => 'left',   align_title => 'left',   title => 'VLAN-Good'},
2140      {is_sep => 1,              title => ' ',       body => ' '},
2141      {align  => 'left',   align_title => 'left',   title => 'VLAN-ID'},
2142      {is_sep => 1,              title => ' ',       body => ' '},
2143      {align  => 'left',   align_title => 'left',   title => 'Date-Last'},
2144      {is_sep => 1,              title => ' ',       body => '  '},
2145      {align  => 'left',   align_title => 'left',   title => 'Date-Good'},
2146      {is_sep => 1,              title => ' ',       body => '  '},
2147      {align  => 'left',   align_title => 'left',   title => 'MAC-Address'},
2148      {is_sep => 1,              title => ' ',       body => ' '},
2149      {align  => 'left',   align_title => 'left',   title => 'Hostname-FQ'},
2150      );
2151
2152   for my $item (@result) {
2153      $tb_bad->add(
2154         $item->{'switch'},
2155         $item->{'port_hr'},
2156         $item->{'vlan_bad'},
2157         $item->{'vlan_good'},
2158         '(' . $item->{'vlan_id'} . ')',
2159         $item->{'date_last'},
2160         $item->{'date_good'},
2161         $item->{'mac_address'},
2162         $item->{'hostname_fq'},
2163         );
2164      }
2165
2166   print $tb_bad->title();
2167   print $tb_bad->rule('-');
2168   print $tb_bad->body();
2169   }
2170
2171#---------------------------------------------------------------
2172sub cmd_bad_vlan_id_html {
2173   my @result = @_;
2174
2175   print <<'END_HTML';
2176<table class="sortable" summary="Klask Bad VLAN ID Database">
2177 <caption>Klask Bad VLAN ID Database</caption>
2178 <thead>
2179  <tr>
2180   <th scope="col" class="klask-header-left">Switch&nbsp;&#8645;</th>
2181   <th scope="col" class="sorttable_nosort">Port</th>
2182   <th scope="col" class="sorttable_alpha">VLAN-Bad&nbsp;&#8645;</th>
2183   <th scope="col" class="sorttable_alpha">VLAN-Good&nbsp;&#8645;</th>
2184   <th scope="col" class="sorttable_alpha">Date-Last&nbsp;&#8645;</th>
2185   <th scope="col" class="sorttable_alpha">Date-Good&nbsp;&#8645;</th>
2186   <th scope="col" class="sorttable_alpha">MAC-Address&nbsp;&#8645;</th>
2187   <th scope="col" class="klask-header-right">Hostname-FQ&nbsp;&#8645;</th>
2188  </tr>
2189 </thead>
2190 <tfoot>
2191  <tr>
2192   <th scope="col" class="klask-footer-left">Switch</th>
2193   <th scope="col" class="fklask-nosort">Port</th>
2194   <th scope="col" class="fklask-alpha">VLAN-Bad</th>
2195   <th scope="col" class="fklask-alpha">VLAN-Good</th>
2196   <th scope="col" class="fklask-alpha">Date-Last</th>
2197   <th scope="col" class="fklask-alpha">Date-Good</th>
2198   <th scope="col" class="fklask-alpha">MAC-Address</th>
2199   <th scope="col" class="klask-footer-right">Hostname-FQ</th>
2200  </tr>
2201 </tfoot>
2202 <tbody>
2203END_HTML
2204
2205   my $typerow = 'even';
2206
2207   for my $item (@result) {
2208
2209      $typerow = $typerow eq 'even' ? 'odd' : 'even';
2210
2211      my $switch_hostname_sort = sprintf '%s %3s' ,$item->{'switch'}, $item->{'port_hr'};
2212      my ( $host_short ) = split m/ \. /xms, $item->{'hostname_fq'};
2213
2214      my $vlan_nameid = $item->{'vlan_good'} . ' (' . $item->{'vlan_id'} . ')';
2215
2216      fqdn_html_breakable(my $hostname_fq_html = $item->{'hostname_fq'});
2217
2218      print <<"END_HTML";
2219  <tr class="$typerow">
2220   <td sorttable_customkey="$switch_hostname_sort">$item->{'switch'}</td>
2221   <td class="bklask-port">$item->{'port_hr'}</td>
2222   <td>!$item->{'vlan_bad'}</td>
2223   <td>$vlan_nameid</td>
2224   <td>$item->{'date_last'}</td>
2225   <td>$item->{'date_good'}</td>
2226   <td>$item->{'mac_address'}</td>
2227   <td sorttable_customkey="$host_short">$hostname_fq_html</td>
2228  </tr>
2229END_HTML
2230      }
2231   print <<'END_HTML';
2232 </tbody>
2233</table>
2234END_HTML
2235   }
2236
2237#---------------------------------------------------------------
2238sub cmd_poe_enable {
2239   @ARGV = @_;
2240
2241   my $verbose;
2242   GetOptions(
2243      'verbose|v' => \$verbose,
2244      );
2245
2246   my $switch_name = shift @ARGV || q{};
2247   my $switch_port = shift @ARGV || q{};
2248
2249   if ($switch_name eq q{} or $switch_port eq q{}) {
2250      die "Usage: klask poe-enable SWITCH_NAME PORT\n";
2251      }
2252
2253   for my $sw_name (split /,/, $switch_name) {
2254      if (not defined $SWITCH_DB{$sw_name}) {
2255         die "Switch $sw_name must be defined in klask configuration file\n";
2256         }
2257
2258      my $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number
2259
2260      my $sw = $SWITCH_DB{$sw_name};
2261      my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2262      print "$error \n" if $error;
2263
2264      my $result = $session->set_request(
2265         -varbindlist => [$oid_search, INTEGER, 8], # Only NEXANS
2266         );
2267      print $session->error()."\n" if $session->error_status();
2268
2269      $session->close;
2270      }
2271   cmd_poe_status($switch_name, $switch_port);
2272   return;
2273   }
2274
2275#---------------------------------------------------------------
2276sub cmd_poe_disable {
2277   @ARGV = @_;
2278
2279   my $verbose;
2280   GetOptions(
2281      'verbose|v' => \$verbose,
2282      );
2283
2284   my $switch_name = shift @ARGV || q{};
2285   my $switch_port = shift @ARGV || q{};
2286
2287   if ($switch_name eq q{} or $switch_port eq q{}) {
2288      die "Usage: klask poe-disable SWITCH_NAME PORT\n";
2289      }
2290
2291   for my $sw_name (split /,/, $switch_name) {
2292      if (not defined $SWITCH_DB{$sw_name}) {
2293         die "Switch $sw_name must be defined in klask configuration file\n";
2294         }
2295
2296      my $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number
2297
2298      my $sw = $SWITCH_DB{$sw_name};
2299      my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2300      print "$error \n" if $error;
2301
2302      my $result = $session->set_request(
2303         -varbindlist => [$oid_search, INTEGER, 2], # Only NEXANS
2304         );
2305      print $session->error()."\n" if $session->error_status();
2306
2307      $session->close;
2308      }
2309   cmd_poe_status($switch_name, $switch_port);
2310   return;
2311   }
2312
2313#---------------------------------------------------------------
2314sub cmd_poe_status {
2315   @ARGV = @_;
2316
2317   my $verbose;
2318   GetOptions(
2319      'verbose|v' => \$verbose,
2320      );
2321
2322   my $switch_name = shift @ARGV || q{};
2323   my $switch_port = shift @ARGV || q{};
2324
2325   if ($switch_name eq q{} or $switch_port eq q{}) {
2326      die "Usage: klask poe-status SWITCH_NAME PORT\n";
2327      }
2328
2329   for my $sw_name (split /,/, $switch_name) {
2330      if (not defined $SWITCH_DB{$sw_name}) {
2331         die "Switch $sw_name must be defined in klask configuration file\n";
2332         }
2333
2334      my $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number
2335
2336      my $sw = $SWITCH_DB{$sw_name};
2337      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
2338      print "$error \n" if $error;
2339
2340      my $result = $session->get_request(
2341         -varbindlist => [$oid_search],
2342         );
2343
2344      if (defined $result and $result->{$oid_search} ne 'noSuchInstance') {
2345         my $poe_status = $result->{$oid_search} || 'empty';
2346         $poe_status =~ s/8/enable/;
2347         $poe_status =~ s/2/disable/;
2348         printf "%s  %s poe %s\n", $sw_name, $switch_port, $poe_status;
2349         }
2350      else {
2351         print "Klask do not find PoE status on switch $sw_name on port $switch_port\n";
2352         }
2353
2354      $session->close;
2355      }
2356   return;
2357   }
2358
2359#---------------------------------------------------------------
2360sub cmd_host_setlocation {
2361   @ARGV = @_;
2362
2363   my ($verbose, $force);
2364   GetOptions(
2365      'verbose|v' => \$verbose,
2366      'force|f'   => \$force,
2367      );
2368
2369   my $switch_name = shift @ARGV || q{};
2370   my $switch_location = shift @ARGV || q{};
2371
2372   if ($switch_name eq q{} or $switch_location eq q{}) {
2373      die "Usage: klask host-setlocation SWITCH_NAME LOCATION\n";
2374      }
2375
2376   for my $sw_name (split /,/, $switch_name) {
2377      if (not defined $SWITCH_DB{$sw_name}) {
2378         die "Switch $sw_name must be defined in klask configuration file\n";
2379         }
2380
2381      my $oid_search = $OID_NUMBER{'sysLocation'};
2382
2383      my $sw = $SWITCH_DB{$sw_name};
2384      my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2385      print "$error \n" if $error;
2386
2387      my $result = $session->set_request(
2388         -varbindlist => [$oid_search, OCTET_STRING, $switch_location],
2389         );
2390      print $session->error()."\n" if $session->error_status();
2391
2392      $session->close;
2393      }
2394   return;
2395   }
2396
2397#---------------------------------------------------------------
2398# not finish - do not use
2399sub cmd_port_setvlan {
2400   my $switch_name = shift || q{};
2401   my $mac_address = shift || q{};
2402
2403   if ($switch_name eq q{} or $mac_address eq q{}) {
2404      die "Usage: klask search-mac-on-switch SWITCH_NAME MAC_ADDRESS\n";
2405      }
2406
2407   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST ) if $switch_name eq q{*};
2408
2409   for my $sw_name (split /,/, $switch_name) {
2410      if (not defined $SWITCH_DB{$sw_name}) {
2411         die "Switch $sw_name must be defined in klask configuration file\n";
2412         }
2413
2414      my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
2415      my $oid_search_port2 = $OID_NUMBER{'searchPort2'} .'.'. 0 . mac_address_hex2dec($mac_address);
2416      print "Klask search OID $oid_search_port1 on switch $sw_name\n";
2417      print "Klask search OID $oid_search_port2 on switch $sw_name\n";
2418
2419      my $sw = $SWITCH_DB{$sw_name};
2420      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
2421      print "$error \n" if $error;
2422
2423      my $result = $session->get_request(
2424         -varbindlist => [$oid_search_port1]
2425         );
2426      if (not defined $result) {
2427         $result = $session->get_request(
2428            -varbindlist => [$oid_search_port2]
2429            );
2430         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
2431         }
2432
2433      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
2434         my $swport = $result->{$oid_search_port1};
2435         print "Klask find MAC $mac_address on switch $sw_name port $swport\n";
2436         }
2437      else {
2438         print "Klask do not find MAC $mac_address on switch $sw_name\n";
2439         }
2440
2441      $session->close;
2442      }
2443   return;
2444   }
2445
2446#---------------------------------------------------------------
2447sub cmd_port_getvlan {
2448   @ARGV = @_;
2449
2450   my $verbose;
2451   GetOptions(
2452      'verbose|v' => \$verbose,
2453      );
2454
2455   my $switch_name = shift @ARGV || q{};
2456   my $switch_port = shift @ARGV || q{};
2457
2458   if ($switch_name eq q{} or $switch_port eq q{}) {
2459      die "Usage: klask port-getvlan SWITCH_NAME PORT\n";
2460      }
2461
2462   for my $sw_name (split /,/, $switch_name) {
2463      if (not defined $SWITCH_DB{$sw_name}) {
2464         die "Switch $sw_name must be defined in klask configuration file\n";
2465         }
2466
2467      my $oid_search = $OID_NUMBER{'vlanPortDefault'} . ".$switch_port";
2468
2469      my $sw = $SWITCH_DB{$sw_name};
2470      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
2471      print "$error \n" if $error;
2472
2473      my $result = $session->get_request(
2474         -varbindlist => [$oid_search],
2475         );
2476
2477      if (defined $result and $result->{$oid_search} ne 'noSuchInstance') {
2478         my $vlan_id = $result->{$oid_search} || 'empty';
2479         print "Klask VLAN Id $vlan_id on switch $sw_name on port $switch_port\n";
2480         }
2481      else {
2482         print "Klask do not find VLAN Id on switch $sw_name on port $switch_port\n";
2483         }
2484
2485      $session->close;
2486      }
2487   return;
2488   }
2489
2490#---------------------------------------------------------------
2491sub cmd_vlan_setname {
2492   }
2493
2494#---------------------------------------------------------------
2495# snmpset -v 1 -c public sw1-batG0-legi.hmg.priv "$OID_NUMBER{'HPicfReset'}.0" i 2;
2496sub cmd_rebootsw {
2497   @ARGV = @_;
2498
2499   my $verbose;
2500   GetOptions(
2501      'verbose|v' => \$verbose,
2502      );
2503
2504   my $switch_name = shift @ARGV || q{};
2505
2506   if ($switch_name eq q{}) {
2507      die "Usage: klask rebootsw SWITCH_NAME\n";
2508      }
2509
2510   for my $sw_name (split /,/, $switch_name) {
2511      if (not defined $SWITCH_DB{$sw_name}) {
2512         die "Switch $sw_name must be defined in klask configuration file\n";
2513         }
2514
2515      my $sw = $SWITCH_DB{$sw_name};
2516      my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2517      print "$error \n" if $error;
2518
2519      my $result = $session->set_request(
2520         -varbindlist => ["$OID_NUMBER{'HPicfReset'}.0", INTEGER, 2],
2521         );
2522
2523      $session->close;
2524      }
2525   return;
2526   }
2527
2528#---------------------------------------------------------------
2529sub cmd_vlan_getname {
2530   my $switch_name = shift || q{};
2531   my $vlan_id     = shift || q{};
2532
2533   if ($switch_name eq q{} or $vlan_id eq q{}) {
2534      die "Usage: klask vlan-getname SWITCH_NAME VLAN_ID\n";
2535      }
2536
2537   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST ) if $switch_name eq q{*};
2538
2539   for my $sw_name (split /,/, $switch_name) {
2540      if (not defined $SWITCH_DB{$sw_name}) {
2541         die "Switch $sw_name must be defined in klask configuration file\n";
2542         }
2543
2544      my $oid_search_vlan_name = $OID_NUMBER{'vlanName'} . ".$vlan_id";
2545
2546      my $sw = $SWITCH_DB{$sw_name};
2547      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
2548      print "$error \n" if $error;
2549
2550      my $result = $session->get_request(
2551         -varbindlist => [$oid_search_vlan_name]
2552         );
2553
2554      if (defined $result and $result->{$oid_search_vlan_name} ne 'noSuchInstance') {
2555         my $vlan_name = $result->{$oid_search_vlan_name} || 'empty';
2556         print "Klask find VLAN $vlan_id on switch $sw_name with name $vlan_name\n";
2557         }
2558      else {
2559         print "Klask do not find VLAN $vlan_id on switch $sw_name\n";
2560         }
2561
2562      $session->close;
2563      }
2564   return;
2565   }
2566
2567#---------------------------------------------------------------
2568sub cmd_vlan_list {
2569   my $switch_name = shift || q{};
2570
2571   if ($switch_name eq q{}) {
2572      die "Usage: klask vlan-list SWITCH_NAME\n";
2573      }
2574
2575   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST ) if $switch_name eq q{*};
2576
2577   for my $sw_name (split /,/, $switch_name) {
2578      if (not defined $SWITCH_DB{$sw_name}) {
2579         die "Switch $sw_name must be defined in klask configuration file\n";
2580         }
2581
2582      my $sw = $SWITCH_DB{$sw_name};
2583      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
2584      print "$error \n" if $error;
2585
2586      my %vlandb = snmp_get_vlan_list($session);
2587      $session->close;
2588
2589      print "VLAN_ID - VLAN_NAME # $sw_name\n";
2590      for my $vlan_id (keys %vlandb) {
2591         printf "%7i - %s\n", $vlan_id, $vlandb{$vlan_id};
2592         }
2593      }
2594   return;
2595   }
2596
2597#---------------------------------------------------------------
2598sub cmd_ip_location {
2599   my $computerdb = computerdb_load();
2600
2601   LOOP_ON_IP_ADDRESS:
2602   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
2603
2604      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
2605
2606      my $sw_hostname = $computerdb->{$ip}{'switch_hostname'} || q{};
2607      next LOOP_ON_IP_ADDRESS if $sw_hostname eq 'unknow';
2608
2609      my $sw_location = q{};
2610      LOOP_ON_ALL_SWITCH:
2611      for my $sw (@SWITCH_LIST) {
2612         next LOOP_ON_ALL_SWITCH if $sw_hostname ne $sw->{'hostname'};
2613         $sw_location = $sw->{'location'};
2614         last;
2615         }
2616
2617      printf "%s: \"%s\"\n", $ip, $sw_location if not $sw_location eq q{};
2618      }
2619   return;
2620   }
2621
2622#---------------------------------------------------------------
2623sub cmd_ip_free {
2624   @ARGV = @_;
2625
2626   my $days_to_death = $DEFAULT{'days-to-death'} || 365 * 2;
2627   my $format = 'txt';
2628   my $verbose;
2629
2630   GetOptions(
2631      'day|d=i'      => \$days_to_death,
2632      'format|f=s'   => \$format,
2633      'verbose|v'    => \$verbose,
2634      );
2635
2636   my %possible_format = (
2637      txt  => \&cmd_ip_free_txt,
2638      html => \&cmd_ip_free_html,
2639      none => sub {},
2640      );
2641   $format = 'txt' if not defined $possible_format{$format};
2642
2643   my @vlan_name = @ARGV;
2644   @vlan_name = get_list_network() if not @vlan_name;
2645
2646   my $computerdb = {};
2647      $computerdb = computerdb_load() if -e "$KLASK_DB_FILE";
2648   my $timestamp = time;
2649
2650   my $timestamp_barrier = $timestamp - (3600 * 24 * $days_to_death);
2651
2652   my %result_ip = ();
2653
2654   ALL_NETWORK:
2655   for my $vlan (@vlan_name) {
2656
2657      my @ip_list = get_list_ip($vlan);
2658
2659      LOOP_ON_IP_ADDRESS:
2660      for my $ip (@ip_list) {
2661
2662         if (exists $computerdb->{$ip}) {
2663            next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'timestamp'} > $timestamp_barrier;
2664
2665            my $mac_address = $computerdb->{$ip}{'mac_address'};
2666            LOOP_ON_DATABASE:
2667            for my $ip_db (keys %{$computerdb}) {
2668               next LOOP_ON_DATABASE if $computerdb->{$ip_db}{'mac_address'} ne $mac_address;
2669               next LOOP_ON_IP_ADDRESS if $computerdb->{$ip_db}{'timestamp'} > $timestamp_barrier;
2670               }
2671            }
2672
2673         my $ip_date_last_detection = '';
2674         if (exists $computerdb->{$ip}) {
2675            my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{'timestamp'};
2676            $year += 1900;
2677            $mon++;
2678            $ip_date_last_detection = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
2679            }
2680
2681         my $packed_ip = scalar gethostbyname($ip);
2682         my $hostname_fq = 'unknown';
2683            $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET) || 'unknown' if defined $packed_ip and get_current_scan_mode($vlan) eq 'active';
2684
2685         next LOOP_ON_IP_ADDRESS if $hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;
2686
2687         $result_ip{$ip} ||= {};
2688         $result_ip{$ip}->{'date_last_detection'} = $ip_date_last_detection;
2689         $result_ip{$ip}->{'hostname_fq'} = $hostname_fq;
2690         $result_ip{$ip}->{'vlan'} = $vlan;
2691
2692         printf "VERBOSE_1: %-15s %-12s %s\n", $ip, $vlan, $hostname_fq if $verbose;
2693         }
2694      }
2695
2696   $possible_format{$format}->(%result_ip);
2697   }
2698
2699#---------------------------------------------------------------
2700sub cmd_ip_free_txt {
2701   my %result_ip = @_;
2702
2703   my $tb_computer = Text::Table->new(
2704      {align => 'left', align_title => 'left', title => 'IPv4-Address'},
2705      {align => 'left', align_title => 'left', title => 'Hostname-FQ'},
2706      {align => 'left', align_title => 'left', title => 'VLAN'},
2707      {align => 'left', align_title => 'left', title => 'Date'},
2708      );
2709
2710   #printf "%-15s %-40s %-16s %s\n", qw(IPv4-Address Hostname-FQ Date VLAN);
2711   #print "-------------------------------------------------------------------------------\n";
2712   LOOP_ON_IP_ADDRESS:
2713   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {
2714      my $vlan_nameid = $result_ip{$ip}->{'vlan'}.'('.get_current_vlan_id($result_ip{$ip}->{'vlan'}).')';
2715      #printf "%-15s %-40s %-16s %s\n", $ip, $result_ip{$ip}->{'hostname_fq'}, $result_ip{$ip}->{'date_last_detection'}, $vlan_nameid;
2716      $tb_computer->add(
2717         $ip,
2718         $result_ip{$ip}->{'hostname_fq'},
2719         $vlan_nameid,
2720         $result_ip{$ip}->{'date_last_detection'},
2721         );
2722      }
2723   print $tb_computer->title();
2724   print $tb_computer->rule('-');
2725   print $tb_computer->body();
2726   }
2727
2728#---------------------------------------------------------------
2729sub cmd_ip_free_html {
2730   my %result_ip = @_;
2731
2732   print <<'END_HTML';
2733<table class="sortable" summary="Klask Free IP Database">
2734 <caption>Klask Free IP Database</caption>
2735 <thead>
2736  <tr>
2737   <th scope="col" class="klask-header-left">IPv4-Address&nbsp;&#8645;</th>
2738   <th scope="col" class="sorttable_alpha">Hostname-FQ&nbsp;&#8645;</th>
2739   <th scope="col" class="sorttable_alpha">VLAN&nbsp;&#8645;</th>
2740   <th scope="col" class="klask-header-right">Date&nbsp;&#8645;</th>
2741  </tr>
2742 </thead>
2743 <tfoot>
2744  <tr>
2745   <th scope="col" class="klask-footer-left">IPv4-Address</th>
2746   <th scope="col" class="fklask-hostname">Hostname-FQ</th>
2747   <th scope="col" class="fklask-vlan">VLAN</th>
2748   <th scope="col" class="klask-footer-right">Date</th>
2749  </tr>
2750 </tfoot>
2751 <tbody>
2752END_HTML
2753
2754   my $typerow = 'even';
2755
2756   LOOP_ON_IP_ADDRESS:
2757   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {
2758
2759      $typerow = $typerow eq 'even' ? 'odd' : 'even';
2760
2761      my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ip;
2762      my ( $host_short ) = split m/ \. /xms, $result_ip{$ip}->{'hostname_fq'};
2763
2764      my $vlan_nameid = $result_ip{$ip}->{'vlan'}.' ('.get_current_vlan_id($result_ip{$ip}->{'vlan'}).')';
2765
2766      fqdn_html_breakable(my $hostname_fq_html = $result_ip{$ip}->{'hostname_fq'});
2767
2768      print <<"END_HTML";
2769  <tr class="$typerow">
2770   <td sorttable_customkey="$ip_sort">$ip</td>
2771   <td sorttable_customkey="$host_short">$hostname_fq_html</td>
2772   <td>$vlan_nameid</td>
2773   <td>$result_ip{$ip}->{'date_last_detection'}</td>
2774  </tr>
2775END_HTML
2776      }
2777   print <<'END_HTML';
2778 </tbody>
2779</table>
2780END_HTML
2781   }
2782
2783#---------------------------------------------------------------
2784sub cmd_enable {
2785   @ARGV = @_;
2786
2787   my $verbose;
2788
2789   GetOptions(
2790      'verbose|v' => \$verbose,
2791      );
2792
2793   my $switch_name = shift @ARGV || q{};
2794   my $port_hr     = shift @ARGV || q{};
2795
2796   if ($switch_name eq q{} or $port_hr eq q{}) {
2797      die "Usage: klask disable SWITCH_NAME PORT\n";
2798      }
2799
2800   if (not defined $SWITCH_DB{$switch_name}) {
2801      die "Switch $switch_name must be defined in klask configuration file\n";
2802      }
2803
2804   my $sw = $SWITCH_DB{$switch_name};
2805   my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2806   print "$error \n" if $error;
2807
2808   # Retrieve numeric port value
2809   my $port_id = snmp_get_switchport_hr2id($session, normalize_port_human_readable($port_hr), $verbose ? 'yes' : '');
2810   die "Error : Port $port_hr does not exist on switch $switch_name\n" if not $port_id =~ m/^\d+$/;
2811
2812   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} .'.'. $port_id;
2813   print "Info: switch $switch_name port $port_hr SNMP OID $oid_search_portstatus\n" if $verbose;
2814
2815   my $result = $session->set_request(
2816      -varbindlist => [$oid_search_portstatus, INTEGER, 1],
2817      );
2818   print $session->error()."\n" if $session->error_status();
2819
2820   $session->close;
2821
2822   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 1 (up)
2823   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 2 (down)
2824   #system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 1";
2825   return;
2826   }
2827
2828#---------------------------------------------------------------
2829sub cmd_disable {
2830   @ARGV = @_;
2831
2832   my $verbose;
2833
2834   GetOptions(
2835      'verbose|v' => \$verbose,
2836      );
2837
2838   my $switch_name = shift @ARGV || q{};
2839   my $port_hr     = shift @ARGV || q{};
2840
2841   if ($switch_name eq q{} or $port_hr eq q{}) {
2842      die "Usage: klask disable SWITCH_NAME PORT\n";
2843      }
2844
2845   if (not defined $SWITCH_DB{$switch_name}) {
2846      die "Switch $switch_name must be defined in klask configuration file\n";
2847      }
2848
2849   my $sw = $SWITCH_DB{$switch_name};
2850   my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2851   print "$error \n" if $error;
2852
2853   # Retrieve numeric port value
2854   my $port_id = snmp_get_switchport_hr2id($session, normalize_port_human_readable($port_hr), $verbose ? 'yes' : '');
2855   die "Error : Port $port_hr does not exist on switch $switch_name\n" if not $port_id =~ m/^\d+$/;
2856
2857   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} .'.'. $port_id;
2858   print "Info: switch $switch_name port $port_hr SNMP OID $oid_search_portstatus\n" if $verbose;
2859
2860   my $result = $session->set_request(
2861      -varbindlist => [$oid_search_portstatus, INTEGER, 2],
2862      );
2863   print $session->error()."\n" if $session->error_status();
2864
2865   $session->close;
2866
2867   #system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 2";
2868   return;
2869   }
2870
2871#---------------------------------------------------------------
2872sub cmd_status {
2873   @ARGV = @_;
2874
2875   my $verbose;
2876
2877   GetOptions(
2878      'verbose|v' => \$verbose,
2879      );
2880
2881   my $switch_name = shift @ARGV || q{};
2882   my $port_hr     = shift @ARGV || q{};
2883
2884   if ($switch_name eq q{} or $port_hr eq q{}) {
2885      die "Usage: klask status SWITCH_NAME PORT\n";
2886      }
2887
2888   if (not defined $SWITCH_DB{$switch_name}) {
2889      die "Switch $switch_name must be defined in klask configuration file\n";
2890      }
2891
2892   my $sw = $SWITCH_DB{$switch_name};
2893   my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
2894   print "$error \n" if $error;
2895
2896   # Retrieve numeric port value
2897   my $port_id = snmp_get_switchport_hr2id($session, normalize_port_human_readable($port_hr), $verbose ? 'yes' : '');
2898   die "Error : Port $port_hr does not exist on switch $switch_name\n" if not $port_id =~ m/^\d+$/;
2899
2900   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} .'.'. $port_id;
2901   print "Info: switch $switch_name port $port_hr ($port_id) SNMP OID $oid_search_portstatus\n" if $verbose;
2902
2903   my $result = $session->get_request(
2904      -varbindlist => [$oid_search_portstatus]
2905      );
2906   print $session->error()."\n" if $session->error_status();
2907   if (defined $result) {
2908      print "$PORT_UPDOWN{$result->{$oid_search_portstatus}}\n";
2909      }
2910
2911   $session->close;
2912
2913   #system "snmpget -v 1 -c public $switch_name 1.3.6.1.2.1.2.2.1.7.$port";
2914   return;
2915   }
2916
2917#---------------------------------------------------------------
2918sub cmd_search_mac_on_switch {
2919   @ARGV = @_;
2920
2921   my $verbose;
2922   my $vlan_id = 0;
2923
2924   GetOptions(
2925      'verbose|v' => \$verbose,
2926      'vlan|l=i'  => \$vlan_id,
2927      );
2928
2929   my $switch_name = shift @ARGV || q{};
2930   my $mac_address = shift @ARGV || q{};
2931
2932   if ($switch_name eq q{} or $mac_address eq q{}) {
2933      die "Usage: klask search-mac-on-switch SWITCH_NAME MAC_ADDRESS\n";
2934      }
2935
2936   $mac_address = normalize_mac_address($mac_address);
2937   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST ) if $switch_name eq q{*} or $switch_name eq q{all};
2938
2939   for my $sw_name (split /,/, $switch_name) {
2940      if (not defined $SWITCH_DB{$sw_name}) {
2941         die "Switch $sw_name must be defined in klask configuration file\n";
2942         }
2943
2944      my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
2945      my $oid_search_port2 = $OID_NUMBER{'searchPort2'} .'.'. $vlan_id . mac_address_hex2dec($mac_address);
2946      print "Klask search OID $oid_search_port1 on switch $sw_name\n" if $verbose;
2947      print "Klask search OID $oid_search_port2 on switch $sw_name\n" if $verbose;
2948
2949      my $sw = $SWITCH_DB{$sw_name};
2950      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
2951      print "$error \n" if $error;
2952
2953      my $result = $session->get_request(
2954         -varbindlist => [$oid_search_port1]
2955         );
2956      if (not defined $result) {
2957         $result = $session->get_request(
2958            -varbindlist => [$oid_search_port2]
2959            );
2960         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
2961         }
2962
2963      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
2964         my $swport_id = $result->{$oid_search_port1};
2965         my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);
2966         print "Klask find MAC $mac_address on switch $sw_name port $swport_hr\n";
2967         }
2968      else {
2969         print "Klask do not find MAC $mac_address on switch $sw_name\n" if $verbose;
2970         }
2971
2972      $session->close;
2973      }
2974   return;
2975   }
2976
2977#---------------------------------------------------------------
2978sub cmd_updatesw {
2979   @ARGV = @_;
2980
2981   my $verbose;
2982
2983   GetOptions(
2984      'verbose|v' => \$verbose,
2985      );
2986   
2987   update_switchdb(verbose => $verbose);
2988   }
2989
2990#---------------------------------------------------------------
2991sub cmd_exportsw {
2992   @ARGV = @_;
2993
2994   test_switchdb_environnement();
2995
2996   my $format = 'txt';
2997   my $graph_modulo = 0;
2998   my $graph_shift  = 1;
2999
3000   GetOptions(
3001      'format|f=s'  => \$format,
3002      'modulo|m=i'  => \$graph_modulo,
3003      'shift|s=i'   => \$graph_shift,
3004      );
3005
3006   my %possible_format = (
3007      txt => \&cmd_exportsw_txt,
3008      dot => \&cmd_exportsw_dot,
3009      );
3010
3011   $format = 'txt' if not defined $possible_format{$format};
3012
3013   $possible_format{$format}->($graph_modulo, $graph_shift, @ARGV);
3014   return;
3015   }
3016
3017#---------------------------------------------------------------
3018sub cmd_exportsw_txt {
3019   my %args = (
3020      way   => 'all',
3021      list  => 0,
3022      @_);
3023
3024   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
3025
3026   my %db_switch_output_port       = %{$switch_connection->{'output_port'}};
3027   my %db_switch_parent            = %{$switch_connection->{'parent'}};
3028   my %db_switch_connected_on_port = %{$switch_connection->{'connected_on_port'}};
3029
3030   # Switch output port and parent port connection
3031   my $tb_child = Text::Table->new( # http://www.perlmonks.org/?node_id=988320
3032      {align => 'left',   align_title => 'left',   title => 'Child-Switch'},
3033      {align => 'right',  align_title => 'right',  title => 'Output-Port'},
3034      {align => 'center', align_title => 'center', title => 'Link'},
3035      {align => 'left',   align_title => 'left',   title => 'Input-Port'},
3036      {align => 'left',   align_title => 'left',   title => 'Parent-Switch'},
3037      );
3038   for my $sw (sort keys %db_switch_output_port) {
3039      my $arrow ='--->';
3040         $arrow ='===>' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
3041      if (exists $db_switch_parent{$sw}) {
3042         $tb_child->add($sw, $db_switch_output_port{$sw}, $arrow, $db_switch_parent{$sw}->{'port_hr'}, $db_switch_parent{$sw}->{'switch'});
3043
3044         }
3045      else {
3046         $tb_child->add($sw, $db_switch_output_port{$sw}, $arrow, '', 'router');
3047         }
3048      }
3049   my @colrange = map { scalar $tb_child->colrange($_) } (0 .. 4); # force scaler context
3050   $tb_child->add(map { ' ' x $_ } reverse @colrange); # add empty line to force symetric table output
3051   print $tb_child->title();
3052   print $tb_child->rule('-');
3053   print $tb_child->body(0, $tb_child->body_height()-1); # remove last fake line
3054   $tb_child->clear;
3055
3056   # Switch parent and children port inter-connection
3057   print "\n";
3058   my $tb_parent = Text::Table->new( # http://www.perlmonks.org/?node_id=988320
3059      {align => 'left',   align_title => 'left',   title => 'Parent-Switch'},
3060      {align => 'right',  align_title => 'right',  title => 'Input-Port'},
3061      {align => 'center', align_title => 'center', title => 'Link'},
3062      {align => 'left',   align_title => 'left',   title => 'Output-Port'},
3063      {align => 'left',   align_title => 'left',   title => 'Child-Switch'},
3064      );
3065   for my $swport (sort keys %db_switch_connected_on_port) {
3066      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
3067      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
3068         my $arrow ='<---';
3069            $arrow ='<===' if $port_connect =~ m/^(Trk|Br|Po)/;
3070         if (exists $db_switch_output_port{$sw}) {
3071            $tb_parent->add($sw_connect, $port_connect, $arrow, $db_switch_output_port{$sw}, $sw);
3072            }
3073         else {
3074            $tb_parent->add($sw_connect, $port_connect, $arrow, '', $sw);
3075            }
3076         }
3077      }
3078   @colrange = map { scalar $tb_parent->colrange($_) } (0 .. 4); # force scaler context
3079   $tb_parent->add(map { ' ' x $_ } reverse @colrange); # add empty line to force symetric table output
3080   print $tb_parent->title();
3081   print $tb_parent->rule('-');
3082   print $tb_parent->body(0, $tb_parent->body_height()-1); # remove last fake line
3083   $tb_parent->clear;
3084
3085   return;
3086   }
3087
3088#---------------------------------------------------------------
3089sub cmd_exportsw_dot {
3090   my $graph_modulo = shift;
3091   my $graph_shift  = shift;
3092
3093   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
3094
3095   my %db_switch_output_port       = %{$switch_connection->{'output_port'}};
3096   my %db_switch_parent            = %{$switch_connection->{'parent'}};
3097   my %db_switch_connected_on_port = %{$switch_connection->{'connected_on_port'}};
3098   my %db_switch_link_with         = %{$switch_connection->{'link_with'}};
3099   my %db_switch_global            = %{$switch_connection->{'switch_db'}};
3100   my $timestamp                   =   $switch_connection->{'timestamp'};
3101
3102   my $invisible_node = 0; # Count number of invisible node
3103
3104   my %db_building= ();
3105   my %db_switch_line = (); # Number of line drawed on a switch
3106   for my $sw (values %db_switch_global) {
3107      my ($building, $location) = split m/ \/ /xms, $sw->{'location'}, 2;
3108      $db_building{$building} ||= {};
3109      $db_building{$building}->{$location} ||= {};
3110      $db_building{$building}->{$location}{ $sw->{'hostname'} } = 'y';
3111
3112      $db_switch_line{$sw} = 0;
3113      }
3114
3115
3116   print "digraph G {\n";
3117   print "rankdir=LR;\n";
3118   #print "splines=polyline;\n";
3119
3120   print "site [label=\"site\", color=black, fillcolor=gold, shape=invhouse, style=filled];\n";
3121   print "internet [label=\"internet\", color=black, fillcolor=cyan, shape=house, style=filled];\n";
3122
3123   my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $timestamp;
3124   $year += 1900;
3125   $mon++;
3126   my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
3127   print "\"$date\" [label=\"MAP DATE\\n\\n$date\", color=white, fillcolor=black, shape=polygon, sides=14, style=filled, fontcolor=white];\n";
3128   print "site -> \"$date\" [style=invis];\n";
3129
3130   my $b=0;
3131   for my $building (keys %db_building) {
3132      $b++;
3133
3134      print "\"building$b\" [label=\"$building\", color=black, fillcolor=gold, style=filled];\n";
3135      print "site -> \"building$b\" [len=2, color=firebrick];\n";
3136
3137      my $l = 0;
3138      for my $loc (keys %{$db_building{$building}}) {
3139         $l++;
3140
3141         print "\"location$b-$l\" [label=\"$building" . q{/} . join(q{\n}, split(m{ / }xms, $loc)) . "\", color=black, fillcolor=orange, style=filled];\n";
3142#         print "\"location$b-$l\" [label=\"$building / $loc\", color=black, fillcolor=orange, style=filled];\n";
3143         print "\"building$b\" -> \"location$b-$l\" [len=2, color=firebrick];\n";
3144
3145         for my $sw (keys %{$db_building{$building}->{$loc}}) {
3146
3147            my $peripheries = 1;
3148            my $color = 'lightblue';
3149            if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
3150               $peripheries = 2;
3151               $color = "\"$color:$color\"";
3152               }
3153            print "\"$sw:$db_switch_output_port{$sw}\" [label=\"".format_aggregator4dot($db_switch_output_port{$sw})."\", color=black, fillcolor=lightblue, peripheries=$peripheries, style=filled];\n";
3154
3155            my $swname  = $sw;
3156               $swname .= q{\n-\n} . "$db_switch_global{$sw}->{'model'}" if exists $db_switch_global{$sw} and exists $db_switch_global{$sw}->{'model'};
3157            print "\"$sw\" [label=\"$swname\", color=black, fillcolor=palegreen, shape=rect, style=filled];\n";
3158            print "\"location$b-$l\" -> \"$sw\" [len=2, color=firebrick, arrowtail=dot];\n";
3159            print "\"$sw\" -> \"$sw:$db_switch_output_port{$sw}\" [len=2, style=bold, color=$color, arrowhead=normal, arrowtail=invdot];\n";
3160
3161
3162            for my $swport (keys %db_switch_connected_on_port) {
3163               my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
3164               next if not $sw_connect eq $sw;
3165               next if $port_connect eq $db_switch_output_port{$sw};
3166               my $peripheries = 1;
3167               my $color = 'plum';
3168               if ($port_connect =~ m/^(Trk|Br|Po)/) {
3169                  $peripheries = 2;
3170                  $color = "\"$color:$color\"";
3171                  }
3172               print "\"$sw:$port_connect\" [label=\"".format_aggregator4dot($port_connect)."\", color=black, fillcolor=plum, peripheries=$peripheries, style=filled];\n";
3173               print "\"$sw:$port_connect\" -> \"$sw\" [len=2, style=bold, color=$color, arrowhead=normal, arrowtail=inv];\n";
3174
3175               #$db_switch_line{$sw}++;
3176               #if ($db_switch_line{$sw} % 9 == 0) {
3177               #   # Create invisible node
3178               #   $invisible_node++;
3179               #   my $invisible = '__Invisible_' . $invisible_node;
3180               #   print "$invisible [shape=none, label=\"\"]\n";
3181               #   print "\"$sw:$port_connect\" -> $invisible [style=invis]\n";
3182               #   print "$invisible            -> \"$sw\"    [style=invis]\n";
3183               #   }
3184              }
3185            }
3186         }
3187      }
3188
3189#   print "Switch output port and parent port connection\n";
3190#   print "---------------------------------------------\n";
3191   for my $sw (sort keys %db_switch_output_port) {
3192      if (exists $db_switch_parent{$sw}) {
3193#         printf "   \"%s:%s\" -> \"%s:%s\"\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{'switch'}, $db_switch_parent{$sw}->{'port_id'};
3194         }
3195      else {
3196         my $style = 'solid';
3197         my $color = 'black'; # navyblue
3198         if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
3199            $style = 'bold';
3200            $color = "\"$color:invis:$color\"";
3201            }
3202         printf "   \"%s:%s\" -> internet [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw};
3203         }
3204      }
3205   print "\n";
3206
3207   # shift graph between 1 or 2 when $graph_shift = 3
3208   my $graph_breaker = 1;
3209
3210#   print "Switch parent and children port inter-connection\n";
3211#   print "------------------------------------------------\n";
3212   for my $swport (sort keys %db_switch_connected_on_port) {
3213      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
3214      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
3215         my $style = 'solid';
3216         my $color = 'black'; # navyblue
3217         if ($port_connect =~ m/^(Trk|Br|Po)/) {
3218            $style = 'bold';
3219            $color = "\"$color:invis:$color\"";
3220            }
3221         if (exists $db_switch_output_port{$sw}) {
3222            printf "   \"%s:%s\" -> \"%s:%s\" [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw}, $sw_connect, $port_connect;
3223
3224            next if $graph_modulo == 0; # No shift (invisible nodes) in graph
3225            $db_switch_line{$sw_connect}++;
3226            if ($db_switch_line{$sw_connect} % $graph_modulo == 0) {
3227               # Create invisible node
3228               $invisible_node++;
3229               my $invisible = '__Invisible_' . $invisible_node;
3230               print  "   \"$invisible.a\" [shape=none, label=\"\"];\n";
3231               printf "   \"%s:%s\"  -> \"$invisible.a\" [style=invis];\n", $sw, $db_switch_output_port{$sw};
3232               $graph_breaker++;
3233               if ($graph_shift == 2 or ($graph_shift == 3 and ($graph_breaker % 2) == 0)) {
3234                  # Two invisible node
3235                  print  "   \"$invisible.b\" [shape=none, label=\"\"];\n";
3236                  print  "   \"$invisible.a\" -> \"$invisible.b\" [style=invis];\n";
3237                  printf "   \"$invisible.b\" -> \"%s:%s\"  [style=invis];\n", $sw_connect, $port_connect;
3238                  }
3239               else {
3240                  # One invisible node
3241                  printf "   \"$invisible.a\" -> \"%s:%s\"  [style=invis];\n", $sw_connect, $port_connect;
3242                  }
3243               }
3244            }
3245         else {
3246            printf "   \"%s\"   -> \"%s:%s\" [style=$style];\n", $sw, $sw_connect, $port_connect;
3247            }
3248         }
3249      }
3250
3251print "}\n";
3252   return;
3253   }
3254
3255
3256################################################################
3257# documentation
3258################################################################
3259
3260__END__
3261
3262=head1 NAME
3263
3264klask - port and search manager for switches, map management
3265
3266
3267=head1 USAGE
3268
3269 klask version
3270 klask help
3271
3272 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
3273 klask exportdb [--format|-f txt|html]
3274 klask removedb IP* computer*
3275 klask cleandb  [--verbose|-v] --day number_of_day --repair-dns
3276
3277 klask updatesw [--verbose|-v]
3278 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY]
3279
3280 klask searchdb [--kind|-k host|mac] computer [mac-address]
3281 klask search   computer
3282 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr
3283
3284 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
3285
3286 klask bad-vlan-id [--day|-d days_before_alert] [--format|-f txt|html]
3287
3288 klask enable  [--verbose|-v] switch port
3289 klask disable [--verbose|-v] switch port
3290 klask status  [--verbose|-v] switch port
3291
3292 klask poe-enable  [--verbose|-v] switch port
3293 klask poe-disable [--verbose|-v] switch port
3294 klask poe-status  [--verbose|-v] switch port
3295
3296 klask vlan-getname switch vlan-id
3297 klask vlan-list switch
3298
3299
3300=head1 DESCRIPTION
3301
3302Klask is a small tool to find where is connected a host in a big network
3303and on which VLAN.
3304Klask mean search in brittany.
3305No hight level protocol like CDL, LLDP are use.
3306Everything is just done with SNMP request on MAC address.
3307
3308Limitation : loop cannot be detected and could be problematic when the map is created (C<updatesw> method).
3309If you use PVST or MSTP and create loop between VLAN,
3310you have to use C<portignore> functionality on switch port to cut manually loop
3311(see config file below).
3312
3313When you use a management port to administrate a switch,
3314it's not possible to create the map with this switch because it does not have a MAC address,
3315so other switch cannot find the real downlink port...
3316One way to work around this problem is, if you have a computer directly connected on the switch,
3317to put this IP as the fake ip for the switch.
3318The MAC address associated will be use just for the map detection.
3319The C<fake-ip> parameter is defined in the config file.
3320
3321Klask has now a web site dedicated for it: L<http://servforge.legi.grenoble-inp.fr/projects/klask>!
3322
3323
3324=head1 COMMANDS
3325
3326Some command are defined in the source code but are not documented here.
3327Theses could be not well defined, not finished, not well tested...
3328You can read the source code and use them at your own risk
3329(like for all the Klask code).
3330
3331=head2 search
3332
3333 klask search   computer
3334
3335This command takes one or more computer in argument.
3336It search a computer on the network and give the port and the switch on which the computer is connected.
3337
3338=head2 search-mac-on-switch
3339
3340 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr
3341
3342This command search a MAC address on a switch.
3343To search on all switch, you could put C<'*'> or C<all>.
3344The VLAN parameter could help.
3345
3346
3347=head2 enable
3348
3349 klask enable  [--verbose|-v] switch port
3350
3351This command activate a port (or an agrregate bridge port) on a switch by SNMP.
3352So you need to give the switch name and a port on the command line.
3353See L</ABBREVIATION FOR PORT>.
3354
3355Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.
3356
3357
3358=head2 disable
3359
3360 klask disable [--verbose|-v] switch port
3361
3362This command deactivate a port (or an agrregate bridge port) on a switch by SNMP.
3363So you need to give the switch name and a port on the command line.
3364See L</ABBREVIATION FOR PORT>.
3365
3366Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.
3367
3368
3369=head2 status
3370
3371 klask status  [--verbose|-v] switch port
3372
3373This command return the status of a port number on a switch by SNMP.
3374The return value could be C<enable> or C<disable> word.
3375So you need to give the switch name and a port on the command line.
3376See L</ABBREVIATION FOR PORT>.
3377
3378If it's not possible to change port status with command L</enable> and L</disable>
3379(SNMP community read write access),
3380it's always possible to have the port status even for bridge agrregate port.
3381
3382
3383=head2 updatedb
3384
3385 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
3386
3387This command will scan networks and update the computer database.
3388To know which are the cmputer scanned, you have to configure the file F</etc/klask/klask.conf>.
3389This file is easy to read and write because Klask use YAML format and not XML
3390(see L</CONFIGURATION>).
3391
3392Option are not stable and could be use manually when you have a new kind of switch.
3393Maybe some option will be transfered in a future C<checksw> command!
3394
3395The network parameter C<scan-mode> can have two values: C<active> or C<passive>.
3396By default, a network is C<active>.
3397This means that an C<fping> command is done at the beginning on all the IP of the network
3398and the computers that was not detected in this pass, but where their Klask entry is less than one week,
3399will have an C<arping>
3400(some OS do not respond to C<ping> but a computer have to respond to C<arping> if it want to interact with other).
3401In the scan mode C<passive>, no C<fping> and no C<arping> are done.
3402It's good for big subnet with few computer (telephone...).
3403The idea of the C<active> scan mode is to force computer to regulary send packet over the network.
3404
3405At the beginning, the command verify that the switch map checksum is always valid.
3406Otherwise, a rebuild procedure will ne done automatically.
3407
3408=head2 exportdb
3409
3410 klask exportdb [--format|-f txt|html]
3411
3412This command print the content of the computer database.
3413There is actually only two format : TXT and HTML.
3414By default, format is TXT.
3415It's very easy to have more format, it's just need times...
3416
3417=head2 removedb
3418
3419 klask removedb IP* computer*
3420
3421This command remove an entry in the database.
3422There is only one kind of parameter, the IP of the computers to be removed.
3423You can put as many IP as you want...
3424
3425Computer DNS names are also a valid entry because a DNS resolver is executed at the beginning.
3426
3427=head2 cleandb
3428
3429 klask cleandb  [--verbose|-v] --day number_of_day --repair-dns
3430
3431Remove double entry (same MAC-Address) in the computer database when the older one is older than X day (C<--day>) the new one.
3432Computer name beginning by 'float' (regex C<^float>) are not really taken into account but could be remove.
3433This could be configure with the global regex parameter C<float-regex> in the configuration file F</etc/klask/klask.conf>.
3434This functionality could be use when computer define in VLAN 1
3435could have a float IP when they are connected on VLAN 2.
3436In the Klask database, the float DNS entries are less important.
3437
3438When reverse DNS has not been done by the past, option C<--repair-dns> force a reverse DNS check on all unkown host.
3439
3440=head2 updatesw
3441
3442 klask updatesw [--verbose|-v]
3443
3444This command build a map of your manageable switch on your network.
3445The list of the switches must be given in the file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
3446
3447The database has a checksum which depend of all the active switches.
3448It's use when rebuilding the database in case of change in switch configuration (one more for example).
3449
3450=head2 exportsw
3451
3452 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY]
3453
3454This command print the content of the switch database. There is actually two format.
3455One is just TXT for terminal and the other is the DOT format from the graphviz environnement.
3456By default, format is TXT.
3457
3458 klask exportsw --format dot > /tmp/map.dot
3459 dot -Tpng /tmp/map.dot > /tmp/map.png
3460
3461In case you have too many switch connected on one switch,
3462the graphviz result graph could be too much vertical.
3463With C<--modulo> > 0, you can specify how many switches (connected on one switch) are on the same columns
3464before shifting them to one column to the left and back again.
3465The C<--shift> parameter must be 1, 2 or 3.
3466With C<--shift> egual to 2, the shift will be to two column to the left.
3467With 3, it will be 1 to the left and 2 to the left one time over two !
3468In practise, we just add virtuals nodes in the dot file,
3469that means the result graph is generated with theses virtuals but invisibles nodes...
3470
3471=head2 ip-free
3472
3473 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
3474
3475This command return IP address that was not use (detected by Klask) at this time.
3476The list returned could be limited to just one VLAN.
3477IP returned could have been never used or no computer have been detected since the number of days specified
3478(2 years by default).
3479This parameter could also be define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
3480
3481 default:
3482   days-to-death: 730
3483
3484Computer that does not have the good IP but takes a float one (see L</cleandb>) are taken into account.
3485
3486
3487=head2 bad-vlan-id
3488
3489 klask bad-vlan-id [--day|-d days_before_alert] [--format|-f txt|html]
3490
3491This command return a list of switch port that are not configure with the good VLAN.
3492Computer which are in bad VLAN are detected with the float regex parameter (see L</cleandb>)
3493and another prior trace where they had the good IP (good DNS name).
3494The computer must stay connected on a bad VLAN more than XX days (15 days by default) before alert.
3495This parameter could also define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
3496
3497 default:
3498   days-before-alert: 15
3499
3500This functionality is not need if your switch use RADIUS 802.1X configuration...
3501
3502
3503=head2 poe-enable
3504
3505 klask poe-enable  [--verbose|-v] switch port
3506
3507This command activate the PoE (Power over Ethernet) on a switch port by SNMP.
3508So you need to give the switch name and a port on the command line.
3509See L</ABBREVIATION FOR PORT>.
3510
3511Warning: Only NEXANS switches are supported (we do not have other switch for testing).
3512You need to have the SNMP write access on the switch in order to modify it's configuration.
3513
3514
3515=head2 poe-disable
3516
3517 klask poe-disable [--verbose|-v] switch port
3518
3519This command deactivate the PoE (Power over Ethernet) on a switch port by SNMP.
3520So you need to give the switch name and a port on the command line.
3521See L</ABBREVIATION FOR PORT>.
3522
3523Warning: Only NEXANS switches are supported (we do not have other switch for testing).
3524You need to have the SNMP write access on the switch in order to modify it's configuration.
3525
3526
3527=head2 poe-status
3528
3529 klask poe-status  [--verbose|-v] switch port
3530
3531This command return the status of the PoE (Power over Ethernet) on a switch port by SNMP.
3532The return value could be C<enable> or C<disable> word.
3533So you need to give the switch name and a port on the command line.
3534See L</ABBREVIATION FOR PORT>.
3535
3536If it's not possible to change the PoE status with command L</poe-enable> and L</poe-disable>
3537(SNMP community read write access),
3538it's always possible to have the PoE port status.
3539
3540Warning: Only NEXANS switches are supported (we do not have other switch for testing).
3541
3542
3543=head1 CONFIGURATION
3544
3545Because Klask need many parameters, it's not possible actually to use command line parameters for everything.
3546The configuration is done in a F</etc/klask/klask.conf> YAML file.
3547This format have many advantage over XML, it's easier to read and to write !
3548
3549Here an example, be aware with indent, it's important in YAML, do not use tabulation !
3550
3551 default:
3552   community: public
3553   community-rw: private
3554   snmpport: 161
3555   float-regex: '(?^msx: ^float )'
3556   scan-mode: active
3557
3558 network:
3559   labnet:
3560     ip-subnet:
3561       - add: 192.168.1.0/24
3562       - add: 192.168.2.0/24
3563     interface: eth0
3564     vlan-id: 12
3565     main-router: gw1.labnet.local
3566
3567   schoolnet:
3568     ip-subnet:
3569       - add: 192.168.3.0/24
3570       - add: 192.168.4.0/24
3571     interface: eth0.38
3572     vlan-id: 13
3573     main-router: gw2.schoolnet.local
3574     scan-mode: passive
3575
3576   etunet:
3577     ip-subnet:
3578       - add: 192.168.5.0/24
3579     interface: eth2
3580     vlan-id: 14
3581     main-router: gw3.etunet.local
3582     scan-mode: passive
3583
3584 switch:
3585   - hostname: sw1.klask.local
3586     location: BatY / 1 floor / K004
3587     portignore:
3588       - 1
3589       - 2
3590
3591   - hostname: sw2.klask.local
3592     location: BatY / 2 floor / K203
3593     type: HP2424
3594     portignore:
3595       - 1
3596       - 2
3597     fake-ip: 192.168.9.14
3598
3599   - hostname: sw3.klask.local
3600     location: BatY / 2 floor / K203
3601
3602I think it's pretty easy to understand.
3603The default section can be overide in any section, if parameter mean something in theses sections.
3604Network to be scan are define in the network section. You must put an add by network.
3605Maybe I will make a delete line to suppress specific computers.
3606The switch section define your switch.
3607You have to write the port number to ignore, this was important if your switchs are cascades
3608(right now, method C<updatesw> find them automatically)
3609and is still important if you have loop (with PVST or MSTP).
3610Just put the ports numbers between switch.
3611
3612The C<community> parameter is use to get SNMP data on switch.
3613It could be overload for each switch.
3614By default, it's value is C<public> and you have to configure a readonly word for safety reason.
3615Some few command change the switch state as the commands L</enable> and L</disable>.
3616In theses rares cases, you need a readwrite SNMP community word define in your configuration file.
3617Klask then use since version C<0.6.2> the C<community-rw> parameter which by default is egal to C<private>.
3618
3619
3620=head1 ABBREVIATION FOR PORT
3621
3622HP Procurve and Nexans switches have a simplistic numbering scheme.
3623It's just number: 1, 2, 3... 24.
3624On HP8000 chassis, ports names begin with an uppercase letter: A1, A2...
3625Nothing is done on theses ports names.
3626
3627On HP Comware and DELL, port digitization schema use a port speed word (generally a very verbose word)
3628followed by tree number.
3629In order to have short name,
3630we made the following rules:
3631
3632 Bridge-Aggregation     -> Br
3633 FastEthernet           -> Fa
3634 Forty-GigabitEthernet  -> Fo
3635 FortyGigabitEthernet   -> Fo
3636 GigabitEthernet        -> Gi
3637 Giga                   -> Gi
3638 Port-Channel           -> Po
3639 Ten-GigabitEthernet    -> Te
3640 TenGigabitEthernet     -> Te
3641 Ten                    -> Te
3642
3643All Klask command automatically normalize the port name on standart output
3644and also on input command line.
3645
3646In the case of use an aggregator port (Po, Tk, Br ...),
3647the real ports used are also return.
3648
3649
3650=head1 SWITCH SUPPORTED
3651
3652Here is a list of switches where Klask gives or gave (for old switches) good results.
3653We have only a few manageable switches to actually test Klask.
3654It is quite possible that switches from other brands will work just as well.
3655You just have to do a test on it and add the line of description that goes well in the source code.
3656Contact us for any additional information.
3657
3658In the following list, the names of the switch types written in parentheses are the code names returned by Klask.
3659This makes it possible to adjust the code names of the different manufacturers!
3660
3661HP: J3299A(HP224M), J4120A(HP1600M), J9029A(HP1800-8G), J9449A(HP1810-8G), J4093A(HP2424M), J9279A(HP2510G-24),
3662J9280A(HP2510G-48), J4813A(HP2524), J4900A(HP2626A), J4900B(HP2626B), J4899B(HP2650), J9021A(HP2810-24G), J9022A(HP2810-48G),
3663J8692A(HP3500-24G), J4903A(HP2824), J4110A(HP8000M), JE074A(HP5120-24G), JE069A(HP5120-48G), JD377A(HP5500-24G), JD374A(HP5500-24F).
3664
3665BayStack: BayStack 350T HW(BS350T)
3666
3667Nexans: GigaSwitch V3 TP SFP-I 48V ES3(NA3483-6G), GigaSwitch V3 TP.PSE.+ 48/54V ES3(NA3483-6P)
3668
3669DELL: PC7024(DPC7024), N2048(DN2048), N4032F(DN4032F), N4064F(DN4064F)
3670
3671H3C and 3COM switches have never not been tested but the new HP Comware switches are exactly the same...
3672
3673H3C: H3C5500
3674
36753COM: 3C17203, 3C17204, 3CR17562-91, 3CR17255-91, 3CR17251-91, 3CR17571-91, 3CRWX220095A, 3CR17254-91, 3CRS48G-24S-91,
36763CRS48G-48S-91, 3C17708, 3C17709, 3C17707, 3CR17258-91, 3CR17181-91, 3CR17252-91, 3CR17253-91, 3CR17250-91, 3CR17561-91,
36773CR17572-91, 3C17702-US, 3C17700.
3678
3679
3680=head1 FILES
3681
3682 /etc/klask/klask.conf
3683 /var/lib/klask/klaskdb
3684 /var/lib/klask/switchdb
3685
3686
3687=head1 SEE ALSO
3688
3689Net::SNMP, Net::Netmask, Net::CIDR::Lite, NetAddr::IP, YAML
3690
3691=over
3692
3693=item * L<Web site|http://servforge.legi.grenoble-inp.fr/projects/klask>
3694
3695=item * L<Online Manual|http://servforge.legi.grenoble-inp.fr/pub/klask/klask.html>
3696
3697=back
3698
3699
3700=head1 VERSION
3701
3702$Id: klask 344 2017-11-01 06:24:34Z g7moreau $
3703
3704
3705=head1 AUTHOR
3706
3707Written by Gabriel Moreau, Grenoble - France
3708
3709
3710=head1 LICENSE AND COPYRIGHT
3711
3712GPL version 2 or later and Perl equivalent
3713
3714Copyright (C) 2005-2017 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>.
Note: See TracBrowser for help on using the repository browser.