source: trunk/klask @ 314

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