source: trunk/klask @ 257

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