source: trunk/klask @ 401

Last change on this file since 401 was 401, checked in by g7moreau, 6 years ago
  • Add script to build Debian package
  • Property svn:executable set to *
  • Property svn:keywords set to Date Author Id Rev
File size: 145.6 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 401 2018-10-02 11:16:30Z 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 = version->declare('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 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},               },
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?)\)},  },
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 401 2018-10-02 11:16:30Z 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   local @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   local @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   local @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   local @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   local @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   local @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      next LOOP_ON_RECENT_COMPUTER if $computerdb->{$src_ip}{'hostname_fq'} eq 'unknow';
2220
2221      my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $switchportdb{$swpt}->{'timestamp'};
2222      $year += 1900;
2223      $mon++;
2224      my $date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;
2225
2226      ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $computerdb->{$src_ip}{'timestamp'};
2227      $year += 1900;
2228      $mon++;
2229      my $src_date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;
2230
2231      my $vlan_id = get_current_vlan_id($computerdb->{$src_ip}{'network'});
2232      my ($switch_hostname, $port_hr) = split /\s+/, $swpt, 2;
2233
2234      push @result, {
2235         switch      => $switch_hostname,
2236         port_hr     => $port_hr,
2237         vlan_bad    => $switchportdb{$swpt}->{'vlan'},
2238         vlan_good   => $computerdb->{$src_ip}{'network'},
2239         vlan_id     => $vlan_id,
2240         date_last   => $date,
2241         date_good   => $src_date,
2242         mac_address => $computerdb->{$src_ip}{'mac_address'},
2243         hostname_fq => $computerdb->{$src_ip}{'hostname_fq'},
2244         };
2245      }
2246
2247   $possible_format{$format}->(@result);
2248   }
2249
2250#---------------------------------------------------------------
2251sub cmd_bad_vlan_id_txt {
2252   my @result = @_;
2253
2254   my $tb_bad = Text::Table->new(
2255      {align  => 'left',   align_title => 'left',   title => 'Switch'},
2256      {is_sep => 1,              title => ' ',       body => ' '},
2257      {align  => 'right',  align_title => 'right',  title => 'Port'},
2258      {is_sep => 1,              title => ' ',       body => ' !'},
2259      {align  => 'left',   align_title => 'left',   title => 'VLAN-Bad'},
2260      {is_sep => 1,              title => ' ',       body => ' +-> '},
2261      {align  => 'left',   align_title => 'left',   title => 'VLAN-Good'},
2262      {is_sep => 1,              title => ' ',       body => ' '},
2263      {align  => 'left',   align_title => 'left',   title => 'VLAN-ID'},
2264      {is_sep => 1,              title => ' ',       body => ' '},
2265      {align  => 'left',   align_title => 'left',   title => 'Date-Last'},
2266      {is_sep => 1,              title => ' ',       body => '  '},
2267      {align  => 'left',   align_title => 'left',   title => 'Date-Good'},
2268      {is_sep => 1,              title => ' ',       body => '  '},
2269      {align  => 'left',   align_title => 'left',   title => 'MAC-Address'},
2270      {is_sep => 1,              title => ' ',       body => ' '},
2271      {align  => 'left',   align_title => 'left',   title => 'Hostname-FQ'},
2272      );
2273
2274   for my $item (@result) {
2275      $tb_bad->add(
2276         $item->{'switch'},
2277         $item->{'port_hr'},
2278         $item->{'vlan_bad'},
2279         $item->{'vlan_good'},
2280         '(' . $item->{'vlan_id'} . ')',
2281         $item->{'date_last'},
2282         $item->{'date_good'},
2283         $item->{'mac_address'},
2284         $item->{'hostname_fq'},
2285         );
2286      }
2287
2288   print $tb_bad->title();
2289   print $tb_bad->rule('-');
2290   print $tb_bad->body();
2291   }
2292
2293#---------------------------------------------------------------
2294sub cmd_bad_vlan_id_html {
2295   my @result = @_;
2296
2297   print <<'END_HTML';
2298<table class="sortable">
2299 <caption>Klask VLAN ID Mismatch Database</caption>
2300 <thead>
2301  <tr>
2302   <th scope="col" class="klask-header-left">Switch</th>
2303   <th scope="col" class="sorttable_nosort">Port</th>
2304   <th scope="col" class="sorttable_alpha">VLAN-Bad</th>
2305   <th scope="col" class="sorttable_alpha">VLAN-Good</th>
2306   <th scope="col" class="sorttable_alpha">Date-Last</th>
2307   <th scope="col" class="sorttable_alpha">Date-Good</th>
2308   <th scope="col" class="sorttable_alpha">MAC-Address</th>
2309   <th scope="col" class="klask-header-right">Hostname-FQ</th>
2310  </tr>
2311 </thead>
2312 <tbody>
2313END_HTML
2314
2315   my $typerow = 'even';
2316
2317   for my $item (@result) {
2318
2319      $typerow = $typerow eq 'even' ? 'odd' : 'even';
2320
2321      my $switch_hostname_sort = format_switchport4sort($item->{'switch'}, $item->{'port_hr'});
2322      my ($host_short) = split m/ \. /xms, $item->{'hostname_fq'};
2323
2324      my $vlan_nameid = $item->{'vlan_good'} . ' (' . $item->{'vlan_id'} . ')';
2325
2326      fqdn_html_breakable(my $hostname_fq_html = $item->{'hostname_fq'});
2327
2328      print <<"END_HTML";
2329  <tr class="$typerow">
2330   <td sorttable_customkey="$switch_hostname_sort">$item->{'switch'}</td>
2331   <td class="bklask-port">$item->{'port_hr'}</td>
2332   <td>!$item->{'vlan_bad'}</td>
2333   <td>$vlan_nameid</td>
2334   <td>$item->{'date_last'}</td>
2335   <td>$item->{'date_good'}</td>
2336   <td>$item->{'mac_address'}</td>
2337   <td sorttable_customkey="$host_short">$hostname_fq_html</td>
2338  </tr>
2339END_HTML
2340      }
2341   print <<'END_HTML';
2342 </tbody>
2343 <tfoot>
2344  <tr>
2345   <th scope="col" class="klask-footer-left">Switch</th>
2346   <th scope="col" class="fklask-nosort">Port</th>
2347   <th scope="col" class="fklask-alpha">VLAN-Bad</th>
2348   <th scope="col" class="fklask-alpha">VLAN-Good</th>
2349   <th scope="col" class="fklask-alpha">Date-Last</th>
2350   <th scope="col" class="fklask-alpha">Date-Good</th>
2351   <th scope="col" class="fklask-alpha">MAC-Address</th>
2352   <th scope="col" class="klask-footer-right">Hostname-FQ</th>
2353  </tr>
2354 </tfoot>
2355</table>
2356END_HTML
2357   }
2358
2359#---------------------------------------------------------------
2360sub cmd_poe_enable {
2361   local @ARGV = @_;
2362
2363   my $verbose;
2364   GetOptions(
2365      'verbose|v' => \$verbose,
2366      );
2367
2368   my $switch_name = shift @ARGV || q{};
2369   my $switch_port = shift @ARGV || q{};
2370
2371   if ($switch_name eq q{} or $switch_port eq q{}) {
2372      die "Usage: klask poe-enable SWITCH_NAME PORT\n";
2373      }
2374
2375   for my $sw_name (split /,/, $switch_name) {
2376      if (not defined $SWITCH_DB{$sw_name}) {
2377         die "Switch $sw_name must be defined in klask configuration file\n";
2378         }
2379
2380      my $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number
2381
2382      my $sw = $SWITCH_DB{$sw_name};
2383      my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2384      print "$error \n" if $error;
2385
2386      my $result = $session->set_request(
2387         -varbindlist => [$oid_search, INTEGER, 8], # Only NEXANS
2388         );
2389      print $session->error() . "\n" if $session->error_status();
2390
2391      $session->close;
2392      }
2393   cmd_poe_status($switch_name, $switch_port);
2394   return;
2395   }
2396
2397#---------------------------------------------------------------
2398sub cmd_poe_disable {
2399   local @ARGV = @_;
2400
2401   my $verbose;
2402   GetOptions(
2403      'verbose|v' => \$verbose,
2404      );
2405
2406   my $switch_name = shift @ARGV || q{};
2407   my $switch_port = shift @ARGV || q{};
2408
2409   if ($switch_name eq q{} or $switch_port eq q{}) {
2410      die "Usage: klask poe-disable SWITCH_NAME PORT\n";
2411      }
2412
2413   for my $sw_name (split /,/, $switch_name) {
2414      if (not defined $SWITCH_DB{$sw_name}) {
2415         die "Switch $sw_name must be defined in klask configuration file\n";
2416         }
2417
2418      my $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number
2419
2420      my $sw = $SWITCH_DB{$sw_name};
2421      my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2422      print "$error \n" if $error;
2423
2424      my $result = $session->set_request(
2425         -varbindlist => [$oid_search, INTEGER, 2], # Only NEXANS
2426         );
2427      print $session->error() . "\n" if $session->error_status();
2428
2429      $session->close;
2430      }
2431   cmd_poe_status($switch_name, $switch_port);
2432   return;
2433   }
2434
2435#---------------------------------------------------------------
2436sub cmd_poe_status {
2437   local @ARGV = @_;
2438
2439   my $verbose;
2440   GetOptions(
2441      'verbose|v' => \$verbose,
2442      );
2443
2444   my $switch_name = shift @ARGV || q{};
2445   my $switch_port = shift @ARGV || q{};
2446
2447   if ($switch_name eq q{} or $switch_port eq q{}) {
2448      die "Usage: klask poe-status SWITCH_NAME PORT\n";
2449      }
2450
2451   for my $sw_name (split /,/, $switch_name) {
2452      if (not defined $SWITCH_DB{$sw_name}) {
2453         die "Switch $sw_name must be defined in klask configuration file\n";
2454         }
2455
2456      my $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number
2457
2458      my $sw = $SWITCH_DB{$sw_name};
2459      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
2460      print "$error \n" if $error;
2461
2462      my $result = $session->get_request(
2463         -varbindlist => [$oid_search],
2464         );
2465
2466      if (defined $result and $result->{$oid_search} ne 'noSuchInstance') {
2467         my $poe_status = $result->{$oid_search} || 'empty';
2468         $poe_status =~ s/8/enable/;
2469         $poe_status =~ s/2/disable/;
2470         printf "%s  %s poe %s\n", $sw_name, $switch_port, $poe_status;
2471         }
2472      else {
2473         print "Klask do not find PoE status on switch $sw_name on port $switch_port\n";
2474         }
2475
2476      $session->close;
2477      }
2478   return;
2479   }
2480
2481#---------------------------------------------------------------
2482sub cmd_host_setlocation {
2483   local @ARGV = @_;
2484
2485   my ($verbose, $force);
2486   GetOptions(
2487      'verbose|v' => \$verbose,
2488      'force|f'   => \$force,
2489      );
2490
2491   my $switch_name     = shift @ARGV || q{};
2492   my $switch_location = shift @ARGV || q{};
2493
2494   if ($switch_name eq q{} or $switch_location eq q{}) {
2495      die "Usage: klask host-setlocation SWITCH_NAME LOCATION\n";
2496      }
2497
2498   for my $sw_name (split /,/, $switch_name) {
2499      if (not defined $SWITCH_DB{$sw_name}) {
2500         die "Switch $sw_name must be defined in klask configuration file\n";
2501         }
2502
2503      my $oid_search = $OID_NUMBER{'sysLocation'};
2504
2505      my $sw = $SWITCH_DB{$sw_name};
2506      my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2507      print "$error \n" if $error;
2508
2509      my $result = $session->set_request(
2510         -varbindlist => [$oid_search, OCTET_STRING, $switch_location],
2511         );
2512      print $session->error()."\n" if $session->error_status();
2513
2514      $session->close;
2515      }
2516   return;
2517   }
2518
2519#---------------------------------------------------------------
2520# not finish - do not use
2521sub cmd_port_setvlan {
2522   my $switch_name = shift || q{};
2523   my $mac_address = shift || q{};
2524
2525   if ($switch_name eq q{} or $mac_address eq q{}) {
2526      die "Usage: klask search-mac-on-switch SWITCH_NAME MAC_ADDRESS\n";
2527      }
2528
2529   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST) if $switch_name eq q{*};
2530
2531   for my $sw_name (split /,/, $switch_name) {
2532      if (not defined $SWITCH_DB{$sw_name}) {
2533         die "Switch $sw_name must be defined in klask configuration file\n";
2534         }
2535
2536      my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
2537      my $oid_search_port2 = $OID_NUMBER{'searchPort2'} . '.' . 0 . mac_address_hex2dec($mac_address);
2538      print "Klask search OID $oid_search_port1 on switch $sw_name\n";
2539      print "Klask search OID $oid_search_port2 on switch $sw_name\n";
2540
2541      my $sw = $SWITCH_DB{$sw_name};
2542      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
2543      print "$error \n" if $error;
2544
2545      my $result = $session->get_request(
2546         -varbindlist => [$oid_search_port1]
2547         );
2548      if (not defined $result) {
2549         $result = $session->get_request(
2550            -varbindlist => [$oid_search_port2]
2551            );
2552         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
2553         }
2554
2555      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
2556         my $swport = $result->{$oid_search_port1};
2557         print "Klask find MAC $mac_address on switch $sw_name port $swport\n";
2558         }
2559      else {
2560         print "Klask do not find MAC $mac_address on switch $sw_name\n";
2561         }
2562
2563      $session->close;
2564      }
2565   return;
2566   }
2567
2568#---------------------------------------------------------------
2569sub cmd_port_getvlan {
2570   local @ARGV = @_;
2571
2572   my $verbose;
2573   GetOptions(
2574      'verbose|v' => \$verbose,
2575      );
2576
2577   my $switch_name = shift @ARGV || q{};
2578   my $switch_port = shift @ARGV || q{};
2579
2580   if ($switch_name eq q{} or $switch_port eq q{}) {
2581      die "Usage: klask port-getvlan SWITCH_NAME PORT\n";
2582      }
2583
2584   for my $sw_name (split /,/, $switch_name) {
2585      if (not defined $SWITCH_DB{$sw_name}) {
2586         die "Switch $sw_name must be defined in klask configuration file\n";
2587         }
2588
2589      my $oid_search = $OID_NUMBER{'vlanPortDefault'} . ".$switch_port";
2590
2591      my $sw = $SWITCH_DB{$sw_name};
2592      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
2593      print "$error \n" if $error;
2594
2595      my $result = $session->get_request(
2596         -varbindlist => [$oid_search],
2597         );
2598
2599      if (defined $result and $result->{$oid_search} ne 'noSuchInstance') {
2600         my $vlan_id = $result->{$oid_search} || 'empty';
2601         print "Klask VLAN Id $vlan_id on switch $sw_name on port $switch_port\n";
2602         }
2603      else {
2604         print "Klask do not find VLAN Id on switch $sw_name on port $switch_port\n";
2605         }
2606
2607      $session->close;
2608      }
2609   return;
2610   }
2611
2612#---------------------------------------------------------------
2613sub cmd_vlan_setname {
2614   }
2615
2616#---------------------------------------------------------------
2617# snmpset -v 1 -c public sw1-batG0-legi.hmg.priv "$OID_NUMBER{'HPicfReset'}.0" i 2;
2618sub cmd_rebootsw {
2619   local @ARGV = @_;
2620
2621   my $verbose;
2622   GetOptions(
2623      'verbose|v' => \$verbose,
2624      );
2625
2626   my $switch_name = shift @ARGV || q{};
2627
2628   if ($switch_name eq q{}) {
2629      die "Usage: klask rebootsw SWITCH_NAME\n";
2630      }
2631
2632   for my $sw_name (split /,/, $switch_name) {
2633      if (not defined $SWITCH_DB{$sw_name}) {
2634         die "Switch $sw_name must be defined in klask configuration file\n";
2635         }
2636
2637      my $sw = $SWITCH_DB{$sw_name};
2638      my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2639      print "$error \n" if $error;
2640
2641      my $result = $session->set_request(
2642         -varbindlist => ["$OID_NUMBER{'HPicfReset'}.0", INTEGER, 2],
2643         );
2644
2645      $session->close;
2646      }
2647   return;
2648   }
2649
2650#---------------------------------------------------------------
2651sub cmd_vlan_getname {
2652   my $switch_name = shift || q{};
2653   my $vlan_id     = shift || q{};
2654
2655   if ($switch_name eq q{} or $vlan_id eq q{}) {
2656      die "Usage: klask vlan-getname SWITCH_NAME VLAN_ID\n";
2657      }
2658
2659   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST) if $switch_name eq q{*};
2660
2661   for my $sw_name (split /,/, $switch_name) {
2662      if (not defined $SWITCH_DB{$sw_name}) {
2663         die "Switch $sw_name must be defined in klask configuration file\n";
2664         }
2665
2666      my $oid_search_vlan_name = $OID_NUMBER{'vlanName'} . ".$vlan_id";
2667
2668      my $sw = $SWITCH_DB{$sw_name};
2669      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
2670      print "$error \n" if $error;
2671
2672      my $result = $session->get_request(
2673         -varbindlist => [$oid_search_vlan_name]
2674         );
2675
2676      if (defined $result and $result->{$oid_search_vlan_name} ne 'noSuchInstance') {
2677         my $vlan_name = $result->{$oid_search_vlan_name} || 'empty';
2678         print "Klask find VLAN $vlan_id on switch $sw_name with name $vlan_name\n";
2679         }
2680      else {
2681         print "Klask do not find VLAN $vlan_id on switch $sw_name\n";
2682         }
2683
2684      $session->close;
2685      }
2686   return;
2687   }
2688
2689#---------------------------------------------------------------
2690sub cmd_vlan_list {
2691   my $switch_name = shift || q{};
2692
2693   if ($switch_name eq q{}) {
2694      die "Usage: klask vlan-list SWITCH_NAME\n";
2695      }
2696
2697   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST) if $switch_name eq q{*};
2698
2699   for my $sw_name (split /,/, $switch_name) {
2700      if (not defined $SWITCH_DB{$sw_name}) {
2701         die "Switch $sw_name must be defined in klask configuration file\n";
2702         }
2703
2704      my $sw = $SWITCH_DB{$sw_name};
2705      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
2706      print "$error \n" if $error;
2707
2708      my %vlandb = snmp_get_vlan_list($session);
2709      $session->close;
2710
2711      print "VLAN_ID - VLAN_NAME # $sw_name\n";
2712      for my $vlan_id (keys %vlandb) {
2713         printf "%7i - %s\n", $vlan_id, $vlandb{$vlan_id};
2714         }
2715      }
2716   return;
2717   }
2718
2719#---------------------------------------------------------------
2720sub cmd_ip_location {
2721   my $computerdb = computerdb_load();
2722
2723   LOOP_ON_IP_ADDRESS:
2724   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
2725
2726      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
2727
2728      my $sw_hostname = $computerdb->{$ip}{'switch_hostname'} || q{};
2729      next LOOP_ON_IP_ADDRESS if $sw_hostname eq 'unknow';
2730
2731      my $sw_location = q{};
2732      LOOP_ON_ALL_SWITCH:
2733      for my $sw (@SWITCH_LIST) {
2734         next LOOP_ON_ALL_SWITCH if $sw_hostname ne $sw->{'hostname'};
2735         $sw_location = $sw->{'location'};
2736         last;
2737         }
2738
2739      printf "%s: \"%s\"\n", $ip, $sw_location if not $sw_location eq q{};
2740      }
2741   return;
2742   }
2743
2744#---------------------------------------------------------------
2745sub cmd_ip_free {
2746   local @ARGV = @_;
2747
2748   my $days_to_death = $DEFAULT{'days-to-death'} || 365 * 2;
2749   my $format = 'txt';
2750   my $verbose;
2751
2752   GetOptions(
2753      'day|d=i'    => \$days_to_death,
2754      'format|f=s' => \$format,
2755      'verbose|v'  => \$verbose,
2756      );
2757
2758   my %possible_format = (
2759      txt  => \&cmd_ip_free_txt,
2760      html => \&cmd_ip_free_html,
2761      none => sub { },
2762      );
2763   $format = 'txt' if not defined $possible_format{$format};
2764
2765   my @vlan_name = @ARGV;
2766   @vlan_name = get_list_network() if not @vlan_name;
2767
2768   my $computerdb = {};
2769   $computerdb = computerdb_load() if -e "$KLASK_DB_FILE";
2770   my $timestamp = time;
2771
2772   my $timestamp_barrier = $timestamp - (3600 * 24 * $days_to_death);
2773
2774   my %result_ip = ();
2775
2776   ALL_NETWORK:
2777   for my $vlan (@vlan_name) {
2778
2779      my @ip_list = get_list_ip($vlan);
2780
2781      LOOP_ON_IP_ADDRESS:
2782      for my $ip (@ip_list) {
2783
2784         if (exists $computerdb->{$ip}) {
2785            next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'timestamp'} > $timestamp_barrier;
2786
2787            my $mac_address = $computerdb->{$ip}{'mac_address'};
2788            LOOP_ON_DATABASE:
2789            for my $ip_db (keys %{$computerdb}) {
2790               next LOOP_ON_DATABASE if $computerdb->{$ip_db}{'mac_address'} ne $mac_address;
2791               next LOOP_ON_IP_ADDRESS if $computerdb->{$ip_db}{'timestamp'} > $timestamp_barrier;
2792               }
2793            }
2794
2795         my $ip_date_last_detection = '';
2796         if (exists $computerdb->{$ip}) {
2797            my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $computerdb->{$ip}{'timestamp'};
2798            $year += 1900;
2799            $mon++;
2800            $ip_date_last_detection = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
2801            }
2802
2803         my $packed_ip   = scalar gethostbyname($ip);
2804         my $hostname_fq = 'unknown';
2805            $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET) || 'unknown' if defined $packed_ip and get_current_scan_mode($vlan) eq 'active';
2806
2807         next LOOP_ON_IP_ADDRESS if $hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;
2808
2809         $result_ip{$ip} ||= {};
2810         $result_ip{$ip}->{'date_last_detection'} = $ip_date_last_detection;
2811         $result_ip{$ip}->{'hostname_fq'}         = $hostname_fq;
2812         $result_ip{$ip}->{'vlan'}                = $vlan;
2813
2814         printf "VERBOSE_1: %-15s %-12s %s\n", $ip, $vlan, $hostname_fq if $verbose;
2815         }
2816      }
2817
2818   $possible_format{$format}->(%result_ip);
2819   }
2820
2821#---------------------------------------------------------------
2822sub cmd_ip_free_txt {
2823   my %result_ip = @_;
2824
2825   my $tb_computer = Text::Table->new(
2826      {align => 'left', align_title => 'left', title => 'IPv4-Address'},
2827      {align => 'left', align_title => 'left', title => 'Hostname-FQ'},
2828      {align => 'left', align_title => 'left', title => 'VLAN'},
2829      {align => 'left', align_title => 'left', title => 'Date'},
2830      );
2831
2832   #printf "%-15s %-40s %-16s %s\n", qw(IPv4-Address Hostname-FQ Date VLAN);
2833   #print "-------------------------------------------------------------------------------\n";
2834   LOOP_ON_IP_ADDRESS:
2835   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {
2836      my $vlan_nameid = $result_ip{$ip}->{'vlan'} . '(' . get_current_vlan_id($result_ip{$ip}->{'vlan'}) . ')';
2837      #printf "%-15s %-40s %-16s %s\n", $ip, $result_ip{$ip}->{'hostname_fq'}, $result_ip{$ip}->{'date_last_detection'}, $vlan_nameid;
2838      $tb_computer->add(
2839         $ip,
2840         $result_ip{$ip}->{'hostname_fq'},
2841         $vlan_nameid,
2842         $result_ip{$ip}->{'date_last_detection'},
2843         );
2844      }
2845   print $tb_computer->title();
2846   print $tb_computer->rule('-');
2847   print $tb_computer->body();
2848   }
2849
2850#---------------------------------------------------------------
2851sub cmd_ip_free_html {
2852   my %result_ip = @_;
2853
2854   print <<'END_HTML';
2855<table class="sortable">
2856 <caption>Klask Free IPv4 Database</caption>
2857 <thead>
2858  <tr>
2859   <th scope="col" class="klask-header-left">IPv4-Address</th>
2860   <th scope="col" class="sorttable_alpha">Hostname-FQ</th>
2861   <th scope="col" class="sorttable_alpha">VLAN</th>
2862   <th scope="col" class="klask-header-right">Date</th>
2863  </tr>
2864 </thead>
2865 <tbody>
2866END_HTML
2867
2868   my $typerow = 'even';
2869
2870   LOOP_ON_IP_ADDRESS:
2871   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {
2872
2873      $typerow = $typerow eq 'even' ? 'odd' : 'even';
2874
2875      my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ip;
2876      my ($host_short) = split m/ \. /xms, $result_ip{$ip}->{'hostname_fq'};
2877
2878      my $vlan_nameid = $result_ip{$ip}->{'vlan'} . ' (' . get_current_vlan_id($result_ip{$ip}->{'vlan'}) . ')';
2879
2880      fqdn_html_breakable(my $hostname_fq_html = $result_ip{$ip}->{'hostname_fq'});
2881
2882      print <<"END_HTML";
2883  <tr class="$typerow">
2884   <td sorttable_customkey="$ip_sort">$ip</td>
2885   <td sorttable_customkey="$host_short">$hostname_fq_html</td>
2886   <td>$vlan_nameid</td>
2887   <td>$result_ip{$ip}->{'date_last_detection'}</td>
2888  </tr>
2889END_HTML
2890      }
2891   print <<'END_HTML';
2892 </tbody>
2893 <tfoot>
2894  <tr>
2895   <th scope="col" class="klask-footer-left">IPv4-Address</th>
2896   <th scope="col" class="fklask-hostname">Hostname-FQ</th>
2897   <th scope="col" class="fklask-vlan">VLAN</th>
2898   <th scope="col" class="klask-footer-right">Date</th>
2899  </tr>
2900 </tfoot>
2901</table>
2902END_HTML
2903   }
2904
2905#---------------------------------------------------------------
2906sub cmd_enable {
2907   local @ARGV = @_;
2908
2909   my $verbose;
2910
2911   GetOptions(
2912      'verbose|v' => \$verbose,
2913      );
2914
2915   my $switch_name = shift @ARGV || q{};
2916   my $port_hr     = shift @ARGV || q{};
2917
2918   if ($switch_name eq q{} or $port_hr eq q{}) {
2919      die "Usage: klask disable SWITCH_NAME PORT\n";
2920      }
2921
2922   if (not defined $SWITCH_DB{$switch_name}) {
2923      die "Switch $switch_name must be defined in klask configuration file\n";
2924      }
2925
2926   my $sw = $SWITCH_DB{$switch_name};
2927   my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2928   print "$error \n" if $error;
2929
2930   # Retrieve numeric port value
2931   my $port_id = snmp_get_switchport_hr2id($session, normalize_port_human_readable($port_hr), $verbose ? 'yes' : '');
2932   die "Error : Port $port_hr does not exist on switch $switch_name\n" if not $port_id =~ m/^\d+$/;
2933
2934   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} . '.' . $port_id;
2935   print "Info: switch $switch_name port $port_hr SNMP OID $oid_search_portstatus\n" if $verbose;
2936
2937   my $result = $session->set_request(
2938      -varbindlist => [$oid_search_portstatus, INTEGER, 1],
2939      );
2940   print $session->error()."\n" if $session->error_status();
2941
2942   $session->close;
2943
2944   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 1 (up)
2945   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 2 (down)
2946   #system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 1";
2947   return;
2948   }
2949
2950#---------------------------------------------------------------
2951sub cmd_disable {
2952   local @ARGV = @_;
2953
2954   my $verbose;
2955
2956   GetOptions(
2957      'verbose|v' => \$verbose,
2958      );
2959
2960   my $switch_name = shift @ARGV || q{};
2961   my $port_hr     = shift @ARGV || q{};
2962
2963   if ($switch_name eq q{} or $port_hr eq q{}) {
2964      die "Usage: klask disable SWITCH_NAME PORT\n";
2965      }
2966
2967   if (not defined $SWITCH_DB{$switch_name}) {
2968      die "Switch $switch_name must be defined in klask configuration file\n";
2969      }
2970
2971   my $sw = $SWITCH_DB{$switch_name};
2972   my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
2973   print "$error \n" if $error;
2974
2975   # Retrieve numeric port value
2976   my $port_id = snmp_get_switchport_hr2id($session, normalize_port_human_readable($port_hr), $verbose ? 'yes' : '');
2977   die "Error : Port $port_hr does not exist on switch $switch_name\n" if not $port_id =~ m/^\d+$/;
2978
2979   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} . '.' . $port_id;
2980   print "Info: switch $switch_name port $port_hr SNMP OID $oid_search_portstatus\n" if $verbose;
2981
2982   my $result = $session->set_request(
2983      -varbindlist => [$oid_search_portstatus, INTEGER, 2],
2984      );
2985   print $session->error()."\n" if $session->error_status();
2986
2987   $session->close;
2988
2989   #system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 2";
2990   return;
2991   }
2992
2993#---------------------------------------------------------------
2994sub cmd_status {
2995   local @ARGV = @_;
2996
2997   my $verbose;
2998
2999   GetOptions(
3000      'verbose|v' => \$verbose,
3001      );
3002
3003   my $switch_name = shift @ARGV || q{};
3004   my $port_hr     = shift @ARGV || q{};
3005
3006   if ($switch_name eq q{} or $port_hr eq q{}) {
3007      die "Usage: klask status SWITCH_NAME PORT\n";
3008      }
3009
3010   if (not defined $SWITCH_DB{$switch_name}) {
3011      die "Switch $switch_name must be defined in klask configuration file\n";
3012      }
3013
3014   my $sw = $SWITCH_DB{$switch_name};
3015   my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
3016   print "$error \n" if $error;
3017
3018   # Retrieve numeric port value
3019   my $port_id = snmp_get_switchport_hr2id($session, normalize_port_human_readable($port_hr), $verbose ? 'yes' : '');
3020   die "Error : Port $port_hr does not exist on switch $switch_name\n" if not $port_id =~ m/^\d+$/;
3021
3022   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} . '.' . $port_id;
3023   print "Info: switch $switch_name port $port_hr ($port_id) SNMP OID $oid_search_portstatus\n" if $verbose;
3024
3025   my $result = $session->get_request(
3026      -varbindlist => [$oid_search_portstatus]
3027      );
3028   print $session->error()."\n" if $session->error_status();
3029   if (defined $result) {
3030      print "$PORT_UPDOWN{$result->{$oid_search_portstatus}}\n";
3031      }
3032
3033   $session->close;
3034
3035   #system "snmpget -v 1 -c public $switch_name 1.3.6.1.2.1.2.2.1.7.$port";
3036   return;
3037   }
3038
3039#---------------------------------------------------------------
3040sub cmd_search_mac_on_switch {
3041   local @ARGV = @_;
3042
3043   my $verbose;
3044   my $vlan_id = 0;
3045
3046   GetOptions(
3047      'verbose|v' => \$verbose,
3048      'vlan|l=i'  => \$vlan_id,
3049      );
3050
3051   my $switch_name = shift @ARGV || q{};
3052   my $mac_address = shift @ARGV || q{};
3053
3054   if ($switch_name eq q{} or $mac_address eq q{}) {
3055      die "Usage: klask search-mac-on-switch SWITCH_NAME MAC_ADDRESS\n";
3056      }
3057
3058   $mac_address = normalize_mac_address($mac_address);
3059   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST) if $switch_name eq q{*} or $switch_name eq q{all};
3060
3061   for my $sw_name (split /,/, $switch_name) {
3062      if (not defined $SWITCH_DB{$sw_name}) {
3063         die "Switch $sw_name must be defined in klask configuration file\n";
3064         }
3065
3066      my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
3067      my $oid_search_port2 = $OID_NUMBER{'searchPort2'} . '.' . $vlan_id . mac_address_hex2dec($mac_address);
3068      print "Klask search OID $oid_search_port1 on switch $sw_name\n" if $verbose;
3069      print "Klask search OID $oid_search_port2 on switch $sw_name\n" if $verbose;
3070
3071      my $sw = $SWITCH_DB{$sw_name};
3072      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
3073      print "$error \n" if $error;
3074
3075      my $result = $session->get_request(
3076         -varbindlist => [$oid_search_port1]
3077         );
3078      if (not defined $result) {
3079         $result = $session->get_request(
3080            -varbindlist => [$oid_search_port2]
3081            );
3082         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
3083         }
3084
3085      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
3086         my $swport_id = $result->{$oid_search_port1};
3087         my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);
3088         print "Klask find MAC $mac_address on switch $sw_name port $swport_hr\n";
3089         }
3090      else {
3091         print "Klask do not find MAC $mac_address on switch $sw_name\n" if $verbose;
3092         }
3093
3094      $session->close;
3095      }
3096   return;
3097   }
3098
3099#---------------------------------------------------------------
3100sub cmd_updatesw {
3101   local @ARGV = @_;
3102
3103   my $verbose;
3104
3105   GetOptions(
3106      'verbose|v' => \$verbose,
3107      );
3108   
3109   update_switchdb(verbose => $verbose);
3110   }
3111
3112#---------------------------------------------------------------
3113sub cmd_exportsw {
3114   local @ARGV = @_;
3115
3116   test_switchdb_environnement();
3117
3118   my $format       = 'txt';
3119   my $graph_modulo = 0;
3120   my $graph_shift  = 1;
3121   my $way          = 'all';
3122   my $no_header;
3123
3124   GetOptions(
3125      'format|f=s'  => \$format,
3126      'modulo|m=i'  => \$graph_modulo,
3127      'shift|s=i'   => \$graph_shift,
3128      'way|w=s'     => \$way,
3129      'no-header|H' => \$no_header,
3130      );
3131
3132   my %possible_format = (
3133      txt => \&cmd_exportsw_txt,
3134      dot => \&cmd_exportsw_dot,
3135      );
3136
3137   $format = 'txt' if not defined $possible_format{$format};
3138
3139   $possible_format{$format}->(modulo => $graph_modulo, shift => $graph_shift, way => $way, 'no-header' => $no_header, @ARGV);
3140   return;
3141   }
3142
3143#---------------------------------------------------------------
3144sub cmd_exportsw_txt {
3145   my %args = (
3146      'way'       => 'all',
3147      'no-header' => undef,
3148      @_);
3149
3150   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
3151   my $interway = 0;
3152
3153   if ($args{'way'} =~ m/all|desc/) {
3154      print "\n" if $interway++;
3155      # Switch description
3156      my %db_switch_global = %{$switch_connection->{'switch_db'}};
3157      my $arrow            = '0--------->>>>';
3158
3159      my $tb_desc = Text::Table->new(
3160         {align => 'left',   align_title => 'left',   title => 'Switch'},
3161         {align => 'center', align_title => 'center', title => 'Is'},
3162         {align => 'left',   align_title => 'left',   title => 'Description'},
3163         {align => 'left',   align_title => 'left',   title => 'Model'},
3164         {align => 'left',   align_title => 'left',   title => 'Revision'},
3165         );
3166
3167      for my $sw (values %db_switch_global) {
3168         $tb_desc->add($sw->{'hostname'}, $arrow, $sw->{'description'}, $sw->{'model'}, $sw->{'revision'});
3169         }
3170
3171      print $tb_desc->title()   unless $args{'no-header'};
3172      print $tb_desc->rule('-') unless $args{'no-header'};
3173      print $tb_desc->body();
3174      $tb_desc->clear;
3175      }
3176
3177   my %db_switch_output_port       = %{$switch_connection->{'output_port'}};
3178   my %db_switch_parent            = %{$switch_connection->{'parent'}};
3179   my %db_switch_connected_on_port = %{$switch_connection->{'connected_on_port'}};
3180
3181   if ($args{'way'} =~ m/all|child/) {
3182      print "\n" if $interway++;
3183      # Switch output port and parent port connection
3184      my $tb_child = Text::Table->new( # http://www.perlmonks.org/?node_id=988320
3185         {align => 'left',   align_title => 'left',   title => 'Child-Switch'},
3186         {align => 'right',  align_title => 'right',  title => 'Output-Port'},
3187         {align => 'center', align_title => 'center', title => 'Link'},
3188         {align => 'left',   align_title => 'left',   title => 'Input-Port'},
3189         {align => 'left',   align_title => 'left',   title => 'Parent-Switch'},
3190         );
3191      for my $sw (sort keys %db_switch_output_port) {
3192         my $arrow = '--->';
3193         $arrow = '===>' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
3194         if (exists $db_switch_parent{$sw}) {
3195            $tb_child->add($sw, $db_switch_output_port{$sw}, $arrow, $db_switch_parent{$sw}->{'port_hr'}, $db_switch_parent{$sw}->{'switch'});
3196
3197            }
3198         else {
3199            $tb_child->add($sw, $db_switch_output_port{$sw}, $arrow, '', 'router');
3200            }
3201         }
3202      my @colrange = map {scalar $tb_child->colrange($_)} (0 .. 4); # force scaler context
3203      $tb_child->add(map {' ' x $_} reverse @colrange);             # add empty line to force symetric table output
3204      print $tb_child->title()   unless $args{'no-header'};
3205      print $tb_child->rule('-') unless $args{'no-header'};
3206      print $tb_child->body(0, $tb_child->body_height() - 1);       # remove last fake line
3207      $tb_child->clear;
3208      }
3209
3210   if ($args{'way'} =~ m/all|parent/) {
3211      print "\n" if $interway++;
3212      # Switch parent and children port inter-connection
3213      my $tb_parent = Text::Table->new(                             # http://www.perlmonks.org/?node_id=988320
3214         {align => 'left',   align_title => 'left',   title => 'Parent-Switch'},
3215         {align => 'right',  align_title => 'right',  title => 'Input-Port'},
3216         {align => 'center', align_title => 'center', title => 'Link'},
3217         {align => 'left',   align_title => 'left',   title => 'Output-Port'},
3218         {align => 'left',   align_title => 'left',   title => 'Child-Switch'},
3219         );
3220      for my $swport (sort keys %db_switch_connected_on_port) {
3221         my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
3222         for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
3223            my $arrow = '<---';
3224            $arrow = '<===' if $port_connect =~ m/^(Trk|Br|Po)/;
3225            if (exists $db_switch_output_port{$sw}) {
3226               $tb_parent->add($sw_connect, $port_connect, $arrow, $db_switch_output_port{$sw}, $sw);
3227               }
3228            else {
3229               $tb_parent->add($sw_connect, $port_connect, $arrow, '', $sw);
3230               }
3231            }
3232         }
3233      my @colrange = map {scalar $tb_parent->colrange($_)} (0 .. 4); # force scaler context
3234      $tb_parent->add(map {' ' x $_} reverse @colrange);             # add empty line to force symetric table output
3235      print $tb_parent->title()   unless $args{'no-header'};
3236      print $tb_parent->rule('-') unless $args{'no-header'};
3237      print $tb_parent->body(0, $tb_parent->body_height() - 1);      # remove last fake line
3238      $tb_parent->clear;
3239      }
3240   return;
3241   }
3242
3243#---------------------------------------------------------------
3244sub cmd_exportsw_dot {
3245   my %args = (
3246      modulo   => 0,
3247      shift    => 1,
3248      @_);
3249
3250   my $graph_modulo = $args{'modulo'};
3251   my $graph_shift  = $args{'shift'};
3252
3253   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
3254
3255   my %db_switch_output_port       = %{$switch_connection->{'output_port'}};
3256   my %db_switch_parent            = %{$switch_connection->{'parent'}};
3257   my %db_switch_connected_on_port = %{$switch_connection->{'connected_on_port'}};
3258   my %db_switch_link_with         = %{$switch_connection->{'link_with'}};
3259   my %db_switch_global            = %{$switch_connection->{'switch_db'}};
3260   my $timestamp                   =   $switch_connection->{'timestamp'};
3261
3262   my $invisible_node = 0; # Count number of invisible node
3263
3264   my %db_building    = ();
3265   my %db_switch_line = (); # Number of line drawed on a switch
3266   for my $sw (values %db_switch_global) {
3267      my ($building, $location) = split m/ \/ /xms, $sw->{'location'}, 2;
3268      $db_building{$building} ||= {};
3269      $db_building{$building}->{$location} ||= {};
3270      $db_building{$building}->{$location}{$sw->{'hostname'}} = 'y';
3271
3272      $db_switch_line{$sw} = 0;
3273      }
3274
3275
3276   print "digraph G {\n";
3277   print "rankdir=LR;\n";
3278   #print "splines=polyline;\n";
3279
3280   print "site [label=\"site\", color=black, fillcolor=gold, shape=invhouse, style=filled];\n";
3281   print "internet [label=\"internet\", color=black, fillcolor=cyan, shape=house, style=filled];\n";
3282
3283   my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $timestamp;
3284   $year += 1900;
3285   $mon++;
3286   my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
3287   print "\"$date\" [label=\"MAP DATE\\n\\n$date\", color=white, fillcolor=black, shape=polygon, sides=14, style=filled, fontcolor=white];\n";
3288   print "site -> \"$date\" [style=invis];\n";
3289
3290   my $b = 0;
3291   for my $building (keys %db_building) {
3292      $b++;
3293
3294      print "\"building$b\" [label=\"$building\", color=black, fillcolor=gold, style=filled];\n";
3295      print "site -> \"building$b\" [len=2, color=firebrick];\n";
3296
3297      my $l = 0;
3298      for my $loc (keys %{$db_building{$building}}) {
3299         $l++;
3300
3301         print "\"location$b-$l\" [label=\"$building" . q{/} . join(q{\n}, split(m{ / }xms, $loc)) . "\", color=black, fillcolor=orange, style=filled];\n";
3302#         print "\"location$b-$l\" [label=\"$building / $loc\", color=black, fillcolor=orange, style=filled];\n";
3303         print "\"building$b\" -> \"location$b-$l\" [len=2, color=firebrick];\n";
3304
3305         for my $sw (keys %{$db_building{$building}->{$loc}}) {
3306
3307            my $peripheries = 1;
3308            my $color       = 'lightblue';
3309            if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
3310               $peripheries = 2;
3311               $color       = "\"$color:$color\"";
3312               }
3313            print "\"$sw:$db_switch_output_port{$sw}\" [label=\"".format_aggregator4dot($db_switch_output_port{$sw})."\", color=black, fillcolor=lightblue, peripheries=$peripheries, style=filled];\n";
3314
3315            my $swname  = $sw;
3316               $swname .= q{\n-\n} . "$db_switch_global{$sw}->{'model'}" if exists $db_switch_global{$sw} and exists $db_switch_global{$sw}->{'model'};
3317            print "\"$sw\" [label=\"$swname\", color=black, fillcolor=palegreen, shape=rect, style=filled];\n";
3318            print "\"location$b-$l\" -> \"$sw\" [len=2, color=firebrick, arrowtail=dot];\n";
3319            print "\"$sw\" -> \"$sw:$db_switch_output_port{$sw}\" [len=2, style=bold, color=$color, arrowhead=normal, arrowtail=invdot];\n";
3320
3321
3322            for my $swport (keys %db_switch_connected_on_port) {
3323               my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
3324               next if not $sw_connect eq $sw;
3325               next if $port_connect eq $db_switch_output_port{$sw};
3326               my $peripheries = 1;
3327               my $color       = 'plum';
3328               if ($port_connect =~ m/^(Trk|Br|Po)/) {
3329                  $peripheries = 2;
3330                  $color       = "\"$color:$color\"";
3331                  }
3332               print "\"$sw:$port_connect\" [label=\"".format_aggregator4dot($port_connect)."\", color=black, fillcolor=plum, peripheries=$peripheries, style=filled];\n";
3333               print "\"$sw:$port_connect\" -> \"$sw\" [len=2, style=bold, color=$color, arrowhead=normal, arrowtail=inv];\n";
3334
3335               #$db_switch_line{$sw}++;
3336               #if ($db_switch_line{$sw} % 9 == 0) {
3337               #   # Create invisible node
3338               #   $invisible_node++;
3339               #   my $invisible = '__Invisible_' . $invisible_node;
3340               #   print "$invisible [shape=none, label=\"\"]\n";
3341               #   print "\"$sw:$port_connect\" -> $invisible [style=invis]\n";
3342               #   print "$invisible            -> \"$sw\"    [style=invis]\n";
3343               #   }
3344               }
3345            }
3346         }
3347      }
3348
3349   #   print "Switch output port and parent port connection\n";
3350   #   print "---------------------------------------------\n";
3351   for my $sw (sort keys %db_switch_output_port) {
3352      if (exists $db_switch_parent{$sw}) {
3353#         printf "   \"%s:%s\" -> \"%s:%s\"\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{'switch'}, $db_switch_parent{$sw}->{'port_id'};
3354         }
3355      else {
3356         my $style = 'solid';
3357         my $color = 'black'; # navyblue
3358         if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
3359            $style = 'bold';
3360            $color = "\"$color:invis:$color\"";
3361            }
3362         printf "   \"%s:%s\" -> internet [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw};
3363         }
3364      }
3365   print "\n";
3366
3367   # shift graph between 1 or 2 when $graph_shift = 3
3368   my $graph_breaker = 1;
3369
3370   #   print "Switch parent and children port inter-connection\n";
3371   #   print "------------------------------------------------\n";
3372   for my $swport (sort keys %db_switch_connected_on_port) {
3373      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
3374      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
3375         my $style = 'solid';
3376         my $color = 'black'; # navyblue
3377         if ($port_connect =~ m/^(Trk|Br|Po)/) {
3378            $style = 'bold';
3379            $color = "\"$color:invis:$color\"";
3380            }
3381         if (exists $db_switch_output_port{$sw}) {
3382            printf "   \"%s:%s\" -> \"%s:%s\" [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw}, $sw_connect, $port_connect;
3383
3384            next if $graph_modulo == 0; # No shift (invisible nodes) in graph
3385            $db_switch_line{$sw_connect}++;
3386            if ($db_switch_line{$sw_connect} % $graph_modulo == 0) {
3387               # Create invisible node
3388               $invisible_node++;
3389               my $invisible = '__Invisible_' . $invisible_node;
3390               print  "   \"$invisible.a\" [shape=none, label=\"\"];\n";
3391               printf "   \"%s:%s\"  -> \"$invisible.a\" [style=invis];\n", $sw, $db_switch_output_port{$sw};
3392               $graph_breaker++;
3393               if ($graph_shift == 2 or ($graph_shift == 3 and ($graph_breaker % 2) == 0)) {
3394                  # Two invisible node
3395                  print  "   \"$invisible.b\" [shape=none, label=\"\"];\n";
3396                  print  "   \"$invisible.a\" -> \"$invisible.b\" [style=invis];\n";
3397                  printf "   \"$invisible.b\" -> \"%s:%s\"  [style=invis];\n", $sw_connect, $port_connect;
3398                  }
3399               else {
3400                  # One invisible node
3401                  printf "   \"$invisible.a\" -> \"%s:%s\"  [style=invis];\n", $sw_connect, $port_connect;
3402                  }
3403               }
3404            }
3405         else {
3406            printf "   \"%s\"   -> \"%s:%s\" [style=$style];\n", $sw, $sw_connect, $port_connect;
3407            }
3408         }
3409      }
3410
3411   print "}\n";
3412   return;
3413   }
3414
3415################################################################
3416# documentation
3417################################################################
3418
3419__END__
3420
3421=head1 NAME
3422
3423klask - port and search manager for switches, map management
3424
3425
3426=head1 USAGE
3427
3428 klask version
3429 klask help
3430
3431 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
3432 klask exportdb [--format|-f txt|html]
3433 klask removedb ipv4_addr* computer*
3434 klask insertdb --ip ipv4_addr --mac mac_addr --network vlan_name
3435 klask cleandb  [--verbose|-v] [--day number_of_day] [--repair-dns]
3436
3437 klask updatesw [--verbose|-v]
3438 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY] [--way all|desc|child|parent] [--no-header|-H]
3439
3440 klask searchdb [--kind|-k host|mac] computer [mac-address]
3441 klask search   computer
3442 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr
3443
3444 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
3445
3446 klask bad-vlan-id [--day|-d days_before_alert] [--format|-f txt|html]
3447
3448 klask enable  [--verbose|-v] switch port
3449 klask disable [--verbose|-v] switch port
3450 klask status  [--verbose|-v] switch port
3451
3452 klask poe-enable  [--verbose|-v] switch port
3453 klask poe-disable [--verbose|-v] switch port
3454 klask poe-status  [--verbose|-v] switch port
3455
3456 klask vlan-getname switch vlan-id
3457 klask vlan-list switch
3458
3459
3460=head1 DESCRIPTION
3461
3462Klask is a small tool to find where is connected a host in a big network
3463and on which VLAN.
3464Klask mean search in brittany.
3465No hight level protocol like CDL, LLDP are use.
3466Everything is just done with SNMP request on MAC address.
3467
3468Limitation : loop cannot be detected and could be problematic when the map is created (C<updatesw> method).
3469If you use PVST or MSTP and create loop between VLAN,
3470you have to use C<portignore> functionality on switch port to cut manually loop
3471(see config file below).
3472
3473When you use a management port to administrate a switch,
3474it's not possible to create the map with this switch because it does not have a MAC address,
3475so other switch cannot find the real downlink port...
3476One way to work around this problem is, if you have a computer directly connected on the switch,
3477to put this IPv4 as the fake ip for the switch.
3478The MAC address associated will be use just for the map detection.
3479The C<fake-ip> parameter is defined in the config file.
3480
3481Klask has now a web site dedicated for it: L<http://servforge.legi.grenoble-inp.fr/projects/klask>!
3482
3483
3484=head1 COMMANDS
3485
3486Some command are defined in the source code but are not documented here.
3487Theses could be not well defined, not finished, not well tested...
3488You can read the source code and use them at your own risk
3489(like for all the Klask code).
3490
3491=head2 search
3492
3493 klask search   computer
3494
3495This command takes one or more computer in argument.
3496It search a computer on the network and give the port and the switch on which the computer is connected.
3497
3498=head2 search-mac-on-switch
3499
3500 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr
3501
3502This command search a MAC address on a switch.
3503To search on all switch, you could put C<'*'> or C<all>.
3504The VLAN parameter could help.
3505
3506
3507=head2 enable
3508
3509 klask enable  [--verbose|-v] switch port
3510
3511This command activate a port (or an agrregate bridge port) on a switch by SNMP.
3512So you need to give the switch name and a port on the command line.
3513See L</ABBREVIATION FOR PORT>.
3514
3515Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.
3516
3517
3518=head2 disable
3519
3520 klask disable [--verbose|-v] switch port
3521
3522This command deactivate a port (or an agrregate bridge port) on a switch by SNMP.
3523So you need to give the switch name and a port on the command line.
3524See L</ABBREVIATION FOR PORT>.
3525
3526Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.
3527
3528
3529=head2 status
3530
3531 klask status  [--verbose|-v] switch port
3532
3533This command return the status of a port number on a switch by SNMP.
3534The return value could be C<enable> or C<disable> word.
3535So you need to give the switch name and a port on the command line.
3536See L</ABBREVIATION FOR PORT>.
3537
3538If it's not possible to change port status with command L</enable> and L</disable>
3539(SNMP community read write access),
3540it's always possible to have the port status even for bridge agrregate port.
3541
3542
3543=head2 updatedb
3544
3545 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
3546
3547This command will scan networks and update the computer database.
3548To know which are the cmputer scanned, you have to configure the file F</etc/klask/klask.conf>.
3549This file is easy to read and write because Klask use YAML format and not XML
3550(see L</CONFIGURATION>).
3551
3552Option are not stable and could be use manually when you have a new kind of switch.
3553Maybe some option will be transfered in a future C<checksw> command!
3554
3555The network parameter C<scan-mode> can have two values: C<active> or C<passive>.
3556By default, a network is C<active>.
3557This means that an C<fping> command is done at the beginning on all the IPv4 of the network
3558and the computers that was not detected in this pass, but where their Klask entry is less than one week,
3559will have an C<arping>
3560(some OS do not respond to C<ping> but a computer have to respond to C<arping> if it want to interact with other).
3561In the scan mode C<passive>, no C<fping> and no C<arping> are done.
3562It's good for big subnet with few computer (telephone...).
3563The idea of the C<active> scan mode is to force computer to regulary send packet over the network.
3564
3565At the beginning, the command verify that the switch map checksum is always valid.
3566Otherwise, a rebuild procedure will ne done automatically.
3567
3568=head2 exportdb
3569
3570 klask exportdb [--format|-f txt|html]
3571
3572This command print the content of the computer database.
3573There is actually only two format : TXT and HTML.
3574By default, format is TXT.
3575It's very easy to have more format, it's just need times...
3576
3577=head2 removedb
3578
3579 klask removedb ipv4_addr* computer*
3580
3581This command remove an entry in the database.
3582There is only one kind of parameter, the IPv4 of the computers to be removed.
3583You can put as many IPv4 as you want...
3584
3585Computer DNS names are also a valid entry because a DNS resolver is executed at the beginning.
3586
3587=head2 insertdb
3588
3589 klask insertdb --ip ipv4_addr --mac mac_addr --network vlan_name
3590
3591This command insert an entry in the database.
3592The IPv4 must not exist previously in the database
3593and the reverse DNS name resolution must be active for that IPv4.
3594
3595The date or timestamp of the entry will be C<number_of_day> in the past (by default 15, see L</cleandb>)
3596from today or from the oldest entry with the same MAC-Address.
3597This command could be use to add pseudo entry and help in the process to detect bad vlan configuration (see L</bad-vlan-id>).
3598
3599=head2 cleandb
3600
3601 klask cleandb  [--verbose|-v] [--day number_of_day] [--repair-dns]
3602
3603Remove double entry (same MAC-Address) in the computer database when the older one is older than X day (C<--day>) the new one.
3604Computer name beginning by 'float' (regex C<^float>) are not really taken into account but could be remove.
3605This could be configure with the global regex parameter C<float-regex> in the configuration file F</etc/klask/klask.conf>.
3606This functionality could be use when computer define in VLAN 1
3607could have a float IPv4 when they are connected on VLAN 2.
3608In the Klask database, the float DNS entries are less important.
3609
3610When reverse DNS has not been done by the past, option C<--repair-dns> force a reverse DNS check on all unkown host.
3611
3612=head2 updatesw
3613
3614 klask updatesw [--verbose|-v]
3615
3616This command build a map of your manageable switch on your network.
3617The list of the switches must be given in the file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
3618
3619The database has a checksum which depend of all the active switches.
3620It's use when rebuilding the database in case of change in switch configuration (one more for example).
3621
3622=head2 exportsw
3623
3624 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY] [--way all|desc|child|parent] [--no-header|-H]
3625
3626This command print the content of the switch database. There is actually two format.
3627One is just TXT for terminal and the other is the DOT format from the graphviz environnement.
3628By default, format is TXT.
3629
3630B<Options for TXT format:>
3631
3632Tree tables are print :
3633C<desc> gives the switches description, model and revision,
3634C<child> return the switch to parent switch table and
3635C<parent> return the switch parent to parent child table.
3636With option C<--way>, you can choose which on to print.
3637C<all> will print all and C<child,parent> will print only C<child> and C<parent> table.
3638With option C<--no-header>, you can remove the header for each table.
3639
3640B<Options for DOT format:>
3641
3642 klask exportsw --format dot > /tmp/map.dot
3643 dot -Tpng /tmp/map.dot > /tmp/map.png
3644
3645In case you have too many switch connected on one switch,
3646the graphviz result graph could be too much vertical.
3647With C<--modulo> > 0, you can specify how many switches (connected on one switch) are on the same columns
3648before shifting them to one column to the left and back again.
3649The C<--shift> parameter must be 1, 2 or 3.
3650With C<--shift> egual to 2, the shift will be to two column to the left.
3651With 3, it will be 1 to the left and 2 to the left one time over two !
3652In practise, we just add virtuals nodes in the dot file,
3653that means the result graph is generated with theses virtuals but invisibles nodes...
3654
3655=head2 ip-free
3656
3657 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
3658
3659This command return IPv4 address that was not use (detected by Klask) at this time.
3660The list returned could be limited to just one VLAN.
3661IP returned could have been never used or no computer have been detected since the number of days specified
3662(2 years by default).
3663This parameter could also be define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
3664
3665 default:
3666   days-to-death: 730
3667
3668Computer that does not have the good IPv4 but takes a float one (see L</cleandb>) are taken into account.
3669
3670
3671=head2 bad-vlan-id
3672
3673 klask bad-vlan-id [--day|-d days_before_alert] [--format|-f txt|html]
3674
3675This command return a list of switch port that are not configure with the good VLAN.
3676Computer which are in bad VLAN are detected with the float regex parameter (see L</cleandb>)
3677and another prior trace where they had the good IPv4 (good DNS name).
3678The computer must stay connected on a bad VLAN more than XX days (15 days by default) before alert.
3679This parameter could also define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
3680
3681 default:
3682   days-before-alert: 15
3683
3684This functionality is not need if your switch use RADIUS 802.1X configuration...
3685
3686
3687=head2 poe-enable
3688
3689 klask poe-enable  [--verbose|-v] switch port
3690
3691This command activate the PoE (Power over Ethernet) on a switch port by SNMP.
3692So you need to give the switch name and a port on the command line.
3693See L</ABBREVIATION FOR PORT>.
3694
3695Warning: Only NEXANS switches are supported (we do not have other switch for testing).
3696You need to have the SNMP write access on the switch in order to modify it's configuration.
3697
3698
3699=head2 poe-disable
3700
3701 klask poe-disable [--verbose|-v] switch port
3702
3703This command deactivate the PoE (Power over Ethernet) on a switch port by SNMP.
3704So you need to give the switch name and a port on the command line.
3705See L</ABBREVIATION FOR PORT>.
3706
3707Warning: Only NEXANS switches are supported (we do not have other switch for testing).
3708You need to have the SNMP write access on the switch in order to modify it's configuration.
3709
3710
3711=head2 poe-status
3712
3713 klask poe-status  [--verbose|-v] switch port
3714
3715This command return the status of the PoE (Power over Ethernet) on a switch port by SNMP.
3716The return value could be C<enable> or C<disable> word.
3717So you need to give the switch name and a port on the command line.
3718See L</ABBREVIATION FOR PORT>.
3719
3720If it's not possible to change the PoE status with command L</poe-enable> and L</poe-disable>
3721(SNMP community read write access),
3722it's always possible to have the PoE port status.
3723
3724Warning: Only NEXANS switches are supported (we do not have other switch for testing).
3725
3726
3727=head1 CONFIGURATION
3728
3729Because Klask need many parameters, it's not possible actually to use command line parameters for everything.
3730The configuration is done in a F</etc/klask/klask.conf> YAML file.
3731This format have many advantage over XML, it's easier to read and to write !
3732
3733Here an example, be aware with indent, it's important in YAML, do not use tabulation !
3734
3735 default:
3736   community: public
3737   community-rw: private
3738   snmpport: 161
3739   float-regex: '(?^msx: ^float )'
3740   scan-mode: active
3741
3742 network:
3743   labnet:
3744     ip-subnet:
3745       - add: 192.168.1.0/24
3746       - add: 192.168.2.0/24
3747     interface: eth0
3748     vlan-id: 12
3749     main-router: gw1.labnet.local
3750
3751   schoolnet:
3752     ip-subnet:
3753       - add: 192.168.3.0/24
3754       - add: 192.168.4.0/24
3755     interface: eth0.38
3756     vlan-id: 13
3757     main-router: gw2.schoolnet.local
3758     scan-mode: passive
3759
3760   etunet:
3761     ip-subnet:
3762       - add: 192.168.5.0/24
3763     interface: eth2
3764     vlan-id: 14
3765     main-router: gw3.etunet.local
3766     scan-mode: passive
3767
3768 switch:
3769   - hostname: sw1.klask.local
3770     location: BatY / 1 floor / K004
3771     portignore:
3772       - 1
3773       - 2
3774
3775   - hostname: sw2.klask.local
3776     location: BatY / 2 floor / K203
3777     type: HP2424
3778     portignore:
3779       - 1
3780       - 2
3781     fake-ip: 192.168.9.14
3782
3783   - hostname: sw3.klask.local
3784     location: BatY / 2 floor / K203
3785
3786I think it's pretty easy to understand.
3787The default section can be overide in any section, if parameter mean something in theses sections.
3788Network to be scan are define in the network section. You must put an add by network.
3789Maybe I will make a delete line to suppress specific computers.
3790The switch section define your switch.
3791You have to write the port number to ignore, this was important if your switchs are cascades
3792(right now, method C<updatesw> find them automatically)
3793and is still important if you have loop (with PVST or MSTP).
3794Just put the ports numbers between switch.
3795
3796The C<community> parameter is use to get SNMP data on switch.
3797It could be overload for each switch.
3798By default, it's value is C<public> and you have to configure a readonly word for safety reason.
3799Some few command change the switch state as the commands L</enable> and L</disable>.
3800In theses rares cases, you need a readwrite SNMP community word define in your configuration file.
3801Klask then use since version C<0.6.2> the C<community-rw> parameter which by default is egal to C<private>.
3802
3803
3804=head1 ABBREVIATION FOR PORT
3805
3806HP Procurve and Nexans switches have a simplistic numbering scheme.
3807It's just number: 1, 2, 3... 24.
3808On HP8000 chassis, ports names begin with an uppercase letter: A1, A2...
3809Nothing is done on theses ports names.
3810
3811On HP Comware and DELL, port digitization schema use a port speed word (generally a very verbose word)
3812followed by tree number.
3813In order to have short name,
3814we made the following rules:
3815
3816 Bridge-Aggregation     -> Br
3817 FastEthernet           -> Fa
3818 Forty-GigabitEthernet  -> Fo
3819 FortyGigabitEthernet   -> Fo
3820 GigabitEthernet        -> Gi
3821 Giga                   -> Gi
3822 Port-Channel           -> Po
3823 Ten-GigabitEthernet    -> Te
3824 TenGigabitEthernet     -> Te
3825 Ten                    -> Te
3826
3827All Klask command automatically normalize the port name on standart output
3828and also on input command line.
3829
3830In the case of use an aggregator port (Po, Tk, Br ...),
3831the real ports used are also return.
3832
3833
3834=head1 SWITCH SUPPORTED
3835
3836Here is a list of switches where Klask gives or gave (for old switches) good results.
3837We have only a few manageable switches to actually test Klask.
3838It is quite possible that switches from other brands will work just as well.
3839You just have to do a test on it and add the line of description that goes well in the source code.
3840Contact us for any additional information.
3841
3842In the following list, the names of the switch types written in parentheses are the code names returned by Klask.
3843This makes it possible to adjust the code names of the different manufacturers!
3844
3845HP: J3299A(HP224M), J4120A(HP1600M), J9029A(HP1800-8G), J9449A(HP1810-8G), J4093A(HP2424M), J9279A(HP2510G-24),
3846J9280A(HP2510G-48), J4813A(HP2524), J4900A(HP2626A), J4900B(HP2626B), J4899B(HP2650), J9021A(HP2810-24G), J9022A(HP2810-48G),
3847J8692A(HP3500-24G), J4903A(HP2824), J4110A(HP8000M), JE074A(HP5120-24G), JE069A(HP5120-48G), JD377A(HP5500-24G), JD374A(HP5500-24F),
3848J4121A(HP4000M), J9145A(HP2910-24G), J3298A(HP212M), J9625A(HP2620-24P).
3849
3850BayStack: BayStack 350T HW(BS350T)
3851
3852Nexans: 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)
3853
3854DELL: PC7024(DPC7024), N2048(DN2048), N4032F(DN4032F), N4064F(DN4064F)
3855
3856H3C and 3COM switches have never not been tested but the new HP Comware switches are exactly the same...
3857
3858H3C: H3C5500
3859
38603COM: 3C17203, 3C17204, 3CR17562-91, 3CR17255-91, 3CR17251-91, 3CR17571-91, 3CRWX220095A, 3CR17254-91, 3CRS48G-24S-91,
38613CRS48G-48S-91, 3C17708, 3C17709, 3C17707, 3CR17258-91, 3CR17181-91, 3CR17252-91, 3CR17253-91, 3CR17250-91, 3CR17561-91,
38623CR17572-91, 3C17702-US, 3C17700.
3863
3864
3865=head1 FILES
3866
3867 /etc/klask/klask.conf
3868 /var/lib/klask/klaskdb
3869 /var/lib/klask/switchdb
3870
3871
3872=head1 SEE ALSO
3873
3874Net::SNMP, Net::Netmask, Net::CIDR::Lite, NetAddr::IP, YAML
3875
3876=over
3877
3878=item * L<Web site|http://servforge.legi.grenoble-inp.fr/projects/klask>
3879
3880=item * L<Online Manual|http://servforge.legi.grenoble-inp.fr/pub/klask/klask.html>
3881
3882=back
3883
3884
3885=head1 VERSION
3886
3887$Id: klask 401 2018-10-02 11:16:30Z g7moreau $
3888
3889
3890=head1 AUTHOR
3891
3892Written by Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>, Grenoble - France
3893
3894=head1 SPECIAL THANKS
3895
3896The list of people below did not directly contribute to Klask's source code
3897but provided me with some data, returned bugs
3898or helped me in another small task like having new ideas ...
3899Maybe I forgot your contribution in recent years,
3900please forgive me in advance and send me an e-mail to correct this.
3901
3902Kevin Reverchon, Olivier De Marchi, Patrick Begou, Herve Colasuonno, David Gras.
3903
3904
3905=head1 LICENSE AND COPYRIGHT
3906
3907License GNU GPL version 2 or later and Perl equivalent
3908
3909Copyright (C) 2005-2018 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>.
Note: See TracBrowser for help on using the repository browser.