source: trunk/klask @ 394

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