source: trunk/klask @ 312

Last change on this file since 312 was 312, checked in by g7moreau, 7 years ago
  • Clean code and change arrow
  • 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 312 2017-10-21 16:59:05Z g7moreau $
6
7use strict;
8use warnings;
9use version; our $VERSION = qv('0.7.4');
10
11use Readonly;
12use FileHandle;
13use Net::SNMP;
14#use YAML;
15use YAML::Syck;
16use Net::Netmask;
17use Net::CIDR::Lite;
18use NetAddr::IP;
19use Getopt::Long qw(GetOptions);
20use Socket;
21use List::Util 'shuffle';
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 312 2017-10-21 16:59:05Z 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>$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><table boder="0" class="noborder"><tr><td>$arrow</td><td>$child_port_hr</td></tr></table></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><table boder="0" class="noborder"><tr><td>$arrow</td><td>$parent_port_hr</td></tr></table></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>$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><table boder="0" class="noborder"><tr><td>$arrow</td><td>$child_port_hr</td></tr></table></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>$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 $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
2851
2852   my %db_switch_output_port       = %{$switch_connection->{'output_port'}};
2853   my %db_switch_parent            = %{$switch_connection->{'parent'}};
2854   my %db_switch_connected_on_port = %{$switch_connection->{'connected_on_port'}};
2855
2856   # Switch output port and parent port connection
2857   my $tb_child = Text::Table->new( # http://www.perlmonks.org/?node_id=988320
2858      {align => 'left',   align_title => 'left',   title => 'Child_Switch'},
2859      {align => 'right',  align_title => 'right',  title => 'Output_Port'},
2860      {align => 'center', align_title => 'center', title => 'Link'},
2861      {align => 'left',   align_title => 'left',   title => 'Input_Port'},
2862      {align => 'left',   align_title => 'left',   title => 'Parent_Switch'},
2863      );
2864   for my $sw (sort keys %db_switch_output_port) {
2865      my $arrow ='--->';
2866         $arrow ='===>' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
2867      if (exists $db_switch_parent{$sw}) {
2868         $tb_child->add($sw, $db_switch_output_port{$sw}, $arrow, $db_switch_parent{$sw}->{'port_hr'}, $db_switch_parent{$sw}->{'switch'});
2869
2870         }
2871      else {
2872         $tb_child->add($sw, $db_switch_output_port{$sw}, $arrow, '', 'router');
2873         }
2874      }
2875   my @colrange = map { scalar $tb_child->colrange($_) } (0 .. 4); # force scaler context
2876   $tb_child->add(map { ' ' x $_ } reverse @colrange); # add empty line to force symetric table output
2877   print $tb_child->title();
2878   print $tb_child->rule('-');
2879   print $tb_child->body(0, $tb_child->body_height()-1); # remove last fake line
2880   $tb_child->clear;
2881
2882   # Switch parent and children port inter-connection
2883   print "\n";
2884   my $tb_parent = Text::Table->new( # http://www.perlmonks.org/?node_id=988320
2885      {align => 'left',   align_title => 'left',   title => 'Parent_Switch'},
2886      {align => 'right',  align_title => 'right',  title => 'Input_Port'},
2887      {align => 'center', align_title => 'center', title => 'Link'},
2888      {align => 'left',   align_title => 'left',   title => 'Output_Port'},
2889      {align => 'left',   align_title => 'left',   title => 'Child_Switch'},
2890      );
2891   for my $swport (sort keys %db_switch_connected_on_port) {
2892      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
2893      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
2894         my $arrow ='<---';
2895            $arrow ='<===' if $port_connect =~ m/^(Trk|Br|Po)/;
2896         if (exists $db_switch_output_port{$sw}) {
2897            $tb_parent->add($sw_connect, $port_connect, $arrow, $db_switch_output_port{$sw}, $sw);
2898            }
2899         else {
2900            $tb_parent->add($sw_connect, $port_connect, $arrow, '', $sw);
2901            }
2902         }
2903      }
2904   @colrange = map { scalar $tb_parent->colrange($_) } (0 .. 4); # force scaler context
2905   $tb_parent->add(map { ' ' x $_ } reverse @colrange); # add empty line to force symetric table output
2906   print $tb_parent->title();
2907   print $tb_parent->rule('-');
2908   print $tb_parent->body(0, $tb_parent->body_height()-1); # remove last fake line
2909   $tb_parent->clear;
2910
2911   return;
2912   }
2913
2914#---------------------------------------------------------------
2915sub cmd_exportsw_dot {
2916   my $graph_modulo = shift;
2917   my $graph_shift  = shift;
2918
2919   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
2920
2921   my %db_switch_output_port       = %{$switch_connection->{'output_port'}};
2922   my %db_switch_parent            = %{$switch_connection->{'parent'}};
2923   my %db_switch_connected_on_port = %{$switch_connection->{'connected_on_port'}};
2924   my %db_switch_link_with         = %{$switch_connection->{'link_with'}};
2925   my %db_switch_global            = %{$switch_connection->{'switch_db'}};
2926   my $timestamp                   =   $switch_connection->{'timestamp'};
2927
2928   my $invisible_node = 0; # Count number of invisible node
2929
2930   my %db_building= ();
2931   my %db_switch_line = (); # Number of line drawed on a switch
2932   for my $sw (values %db_switch_global) {
2933      my ($building, $location) = split m/ \/ /xms, $sw->{'location'}, 2;
2934      $db_building{$building} ||= {};
2935      $db_building{$building}->{$location} ||= {};
2936      $db_building{$building}->{$location}{ $sw->{'hostname'} } = 'y';
2937
2938      $db_switch_line{$sw} = 0;
2939      }
2940
2941
2942   print "digraph G {\n";
2943   print "rankdir=LR;\n";
2944   #print "splines=polyline;\n";
2945
2946   print "site [label=\"site\", color=black, fillcolor=gold, shape=invhouse, style=filled];\n";
2947   print "internet [label=\"internet\", color=black, fillcolor=cyan, shape=house, style=filled];\n";
2948
2949   my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $timestamp;
2950   $year += 1900;
2951   $mon++;
2952   my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
2953   print "\"$date\" [label=\"MAP DATE\\n\\n$date\", color=white, fillcolor=black, shape=polygon, sides=14, style=filled, fontcolor=white];\n";
2954   print "site -> \"$date\" [style=invis];\n";
2955
2956   my $b=0;
2957   for my $building (keys %db_building) {
2958      $b++;
2959
2960      print "\"building$b\" [label=\"$building\", color=black, fillcolor=gold, style=filled];\n";
2961      print "site -> \"building$b\" [len=2, color=firebrick];\n";
2962
2963      my $l = 0;
2964      for my $loc (keys %{$db_building{$building}}) {
2965         $l++;
2966
2967         print "\"location$b-$l\" [label=\"$building" . q{/} . join(q{\n}, split(m{ / }xms, $loc)) . "\", color=black, fillcolor=orange, style=filled];\n";
2968#         print "\"location$b-$l\" [label=\"$building / $loc\", color=black, fillcolor=orange, style=filled];\n";
2969         print "\"building$b\" -> \"location$b-$l\" [len=2, color=firebrick];\n";
2970
2971         for my $sw (keys %{$db_building{$building}->{$loc}}) {
2972
2973            my $peripheries = 1;
2974            my $color = 'lightblue';
2975            if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
2976               $peripheries = 2;
2977               $color = "\"$color:$color\"";
2978               }
2979            print "\"$sw:$db_switch_output_port{$sw}\" [label=\"".format_aggregator4dot($db_switch_output_port{$sw})."\", color=black, fillcolor=lightblue, peripheries=$peripheries, style=filled];\n";
2980
2981            my $swname  = $sw;
2982               $swname .= q{\n-\n} . "$db_switch_global{$sw}->{'model'}" if exists $db_switch_global{$sw} and exists $db_switch_global{$sw}->{'model'};
2983            print "\"$sw\" [label=\"$swname\", color=black, fillcolor=palegreen, shape=rect, style=filled];\n";
2984            print "\"location$b-$l\" -> \"$sw\" [len=2, color=firebrick, arrowtail=dot];\n";
2985            print "\"$sw\" -> \"$sw:$db_switch_output_port{$sw}\" [len=2, style=bold, color=$color, arrowhead=normal, arrowtail=invdot];\n";
2986
2987
2988            for my $swport (keys %db_switch_connected_on_port) {
2989               my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
2990               next if not $sw_connect eq $sw;
2991               next if $port_connect eq $db_switch_output_port{$sw};
2992               my $peripheries = 1;
2993               my $color = 'plum';
2994               if ($port_connect =~ m/^(Trk|Br|Po)/) {
2995                  $peripheries = 2;
2996                  $color = "\"$color:$color\"";
2997                  }
2998               print "\"$sw:$port_connect\" [label=\"".format_aggregator4dot($port_connect)."\", color=black, fillcolor=plum, peripheries=$peripheries, style=filled];\n";
2999               print "\"$sw:$port_connect\" -> \"$sw\" [len=2, style=bold, color=$color, arrowhead=normal, arrowtail=inv];\n";
3000
3001               #$db_switch_line{$sw}++;
3002               #if ($db_switch_line{$sw} % 9 == 0) {
3003               #   # Create invisible node
3004               #   $invisible_node++;
3005               #   my $invisible = '__Invisible_' . $invisible_node;
3006               #   print "$invisible [shape=none, label=\"\"]\n";
3007               #   print "\"$sw:$port_connect\" -> $invisible [style=invis]\n";
3008               #   print "$invisible            -> \"$sw\"    [style=invis]\n";
3009               #   }
3010              }
3011            }
3012         }
3013      }
3014
3015#   print "Switch output port and parent port connection\n";
3016#   print "---------------------------------------------\n";
3017   for my $sw (sort keys %db_switch_output_port) {
3018      if (exists $db_switch_parent{$sw}) {
3019#         printf "   \"%s:%s\" -> \"%s:%s\"\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{'switch'}, $db_switch_parent{$sw}->{'port_id'};
3020         }
3021      else {
3022         my $style = 'solid';
3023         my $color = 'black'; # navyblue
3024         if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
3025            $style = 'bold';
3026            $color = "\"$color:invis:$color\"";
3027            }
3028         printf "   \"%s:%s\" -> internet [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw};
3029         }
3030      }
3031   print "\n";
3032
3033   # shift graph between 1 or 2 when $graph_shift = 3
3034   my $graph_breaker = 1;
3035
3036#   print "Switch parent and children port inter-connection\n";
3037#   print "------------------------------------------------\n";
3038   for my $swport (sort keys %db_switch_connected_on_port) {
3039      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
3040      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
3041         my $style = 'solid';
3042         my $color = 'black'; # navyblue
3043         if ($port_connect =~ m/^(Trk|Br|Po)/) {
3044            $style = 'bold';
3045            $color = "\"$color:invis:$color\"";
3046            }
3047         if (exists $db_switch_output_port{$sw}) {
3048            printf "   \"%s:%s\" -> \"%s:%s\" [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw}, $sw_connect, $port_connect;
3049
3050            next if $graph_modulo == 0; # No shift (invisible nodes) in graph
3051            $db_switch_line{$sw_connect}++;
3052            if ($db_switch_line{$sw_connect} % $graph_modulo == 0) {
3053               # Create invisible node
3054               $invisible_node++;
3055               my $invisible = '__Invisible_' . $invisible_node;
3056               print  "   \"$invisible.a\" [shape=none, label=\"\"];\n";
3057               printf "   \"%s:%s\"  -> \"$invisible.a\" [style=invis];\n", $sw, $db_switch_output_port{$sw};
3058               $graph_breaker++;
3059               if ($graph_shift == 2 or ($graph_shift == 3 and ($graph_breaker % 2) == 0)) {
3060                  # Two invisible node
3061                  print  "   \"$invisible.b\" [shape=none, label=\"\"];\n";
3062                  print  "   \"$invisible.a\" -> \"$invisible.b\" [style=invis];\n";
3063                  printf "   \"$invisible.b\" -> \"%s:%s\"  [style=invis];\n", $sw_connect, $port_connect;
3064                  }
3065               else {
3066                  # One invisible node
3067                  printf "   \"$invisible.a\" -> \"%s:%s\"  [style=invis];\n", $sw_connect, $port_connect;
3068                  }
3069               }
3070            }
3071         else {
3072            printf "   \"%s\"   -> \"%s:%s\" [style=$style];\n", $sw, $sw_connect, $port_connect;
3073            }
3074         }
3075      }
3076
3077print "}\n";
3078   return;
3079   }
3080
3081
3082################################################################
3083# documentation
3084################################################################
3085
3086__END__
3087
3088=head1 NAME
3089
3090klask - port and search manager for switches, map management
3091
3092
3093=head1 USAGE
3094
3095 klask version
3096 klask help
3097
3098 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
3099 klask exportdb [--format|-f txt|html]
3100 klask removedb IP* computer*
3101 klask cleandb  [--verbose|-v] --day number_of_day --repair-dns
3102
3103 klask updatesw [--verbose|-v]
3104 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY]
3105
3106 klask searchdb [--kind|-k host|mac] computer [mac-address]
3107 klask search   computer
3108 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr
3109
3110 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
3111
3112 klask bad-vlan-id [--day|-d days_before_alert]
3113
3114 klask enable  [--verbose|-v] switch port
3115 klask disable [--verbose|-v] switch port
3116 klask status  [--verbose|-v] switch port
3117
3118 klask poe-enable  [--verbose|-v] switch port
3119 klask poe-disable [--verbose|-v] switch port
3120 klask poe-status  [--verbose|-v] switch port
3121
3122 klask vlan-getname switch vlan-id
3123 klask vlan-list switch
3124
3125
3126=head1 DESCRIPTION
3127
3128Klask is a small tool to find where is connected a host in a big network
3129and on which VLAN.
3130Klask mean search in brittany.
3131No hight level protocol like CDL, LLDP are use.
3132Everything is just done with SNMP request on MAC address.
3133
3134Limitation : loop cannot be detected and could be problematic when the map is created (C<updatesw> method).
3135If you use PVST or MSTP and create loop between VLAN,
3136you have to use C<portignore> functionality on switch port to cut manually loop
3137(see config file below).
3138
3139When you use a management port to administrate a switch,
3140it's not possible to create the map with this switch because it does not have a MAC address,
3141so other switch cannot find the real downlink port...
3142One way to work around this problem is, if you have a computer directly connected on the switch,
3143to put this IP as the fake ip for the switch.
3144The MAC address associated will be use just for the map detection.
3145The C<fake-ip> parameter is defined in the config file.
3146
3147Klask has now a web site dedicated for it: L<http://servforge.legi.grenoble-inp.fr/projects/klask>!
3148
3149
3150=head1 COMMANDS
3151
3152Some command are defined in the source code but are not documented here.
3153Theses could be not well defined, not finished, not well tested...
3154You can read the source code and use them at your own risk
3155(like for all the Klask code).
3156
3157=head2 search
3158
3159 klask search   computer
3160
3161This command takes one or more computer in argument.
3162It search a computer on the network and give the port and the switch on which the computer is connected.
3163
3164=head2 search-mac-on-switch
3165
3166 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr
3167
3168This command search a MAC address on a switch.
3169To search on all switch, you could put C<'*'> or C<all>.
3170The VLAN parameter could help.
3171
3172
3173=head2 enable
3174
3175 klask enable  [--verbose|-v] switch port
3176
3177This command activate a port (or an agrregate bridge port) on a switch by SNMP.
3178So you need to give the switch name and a port on the command line.
3179See L</ABBREVIATION FOR PORT>.
3180
3181Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.
3182
3183
3184=head2 disable
3185
3186 klask disable [--verbose|-v] switch port
3187
3188This command deactivate a port (or an agrregate bridge port) on a switch by SNMP.
3189So you need to give the switch name and a port on the command line.
3190See L</ABBREVIATION FOR PORT>.
3191
3192Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.
3193
3194
3195=head2 status
3196
3197 klask status  [--verbose|-v] switch port
3198
3199This command return the status of a port number on a switch by SNMP.
3200The return value could be C<enable> or C<disable> word.
3201So you need to give the switch name and a port on the command line.
3202See L</ABBREVIATION FOR PORT>.
3203
3204If it's not possible to change port status with command L</enable> and L</disable>
3205(SNMP community read write access),
3206it's always possible to have the port status even for bridge agrregate port.
3207
3208
3209=head2 updatedb
3210
3211 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
3212
3213This command will scan networks and update the computer database.
3214To know which are the cmputer scanned, you have to configure the file F</etc/klask/klask.conf>.
3215This file is easy to read and write because Klask use YAML format and not XML
3216(see L</CONFIGURATION>).
3217
3218Option are not stable and could be use manually when you have a new kind of switch.
3219Maybe some option will be transfered in a future C<checksw> command!
3220
3221The network parameter C<scan-mode> can have two values: C<active> or C<passive>.
3222By default, a network is C<active>.
3223This means that an C<fping> command is done at the beginning on all the IP of the network
3224and the computers that was not detected in this pass, but where their Klask entry is less than one week,
3225will have an C<arping>
3226(some OS do not respond to C<ping> but a computer have to respond to C<arping> if it want to interact with other).
3227In the scan mode C<passive>, no C<fping> and no C<arping> are done.
3228It's good for big subnet with few computer (telephone...).
3229The idea of the C<active> scan mode is to force computer to regulary send packet over the network.
3230
3231At the beginning, the command verify that the switch map checksum is always valid.
3232Otherwise, a rebuild procedure will ne done automatically.
3233
3234=head2 exportdb
3235
3236 klask exportdb [--format|-f txt|html]
3237
3238This command print the content of the computer database.
3239There is actually only two format : TXT and HTML.
3240By default, format is TXT.
3241It's very easy to have more format, it's just need times...
3242
3243=head2 removedb
3244
3245 klask removedb IP* computer*
3246
3247This command remove an entry in the database.
3248There is only one kind of parameter, the IP of the computers to be removed.
3249You can put as many IP as you want...
3250
3251Computer DNS names are also a valid entry because a DNS resolver is executed at the beginning.
3252
3253=head2 cleandb
3254
3255 klask cleandb  [--verbose|-v] --day number_of_day --repair-dns
3256
3257Remove double entry (same MAC-Address) in the computer database when the older one is older than X day (C<--day>) the new one.
3258Computer name beginning by 'float' (regex C<^float>) are not really taken into account but could be remove.
3259This could be configure with the global regex parameter C<float-regex> in the configuration file F</etc/klask/klask.conf>.
3260This functionality could be use when computer define in VLAN 1
3261could have a float IP when they are connected on VLAN 2.
3262In the Klask database, the float DNS entries are less important.
3263
3264When reverse DNS has not been done by the past, option C<--repair-dns> force a reverse DNS check on all unkown host.
3265
3266=head2 updatesw
3267
3268 klask updatesw [--verbose|-v]
3269
3270This command build a map of your manageable switch on your network.
3271The list of the switches must be given in the file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
3272
3273The database has a checksum which depend of all the active switches.
3274It's use when rebuilding the database in case of change in switch configuration (one more for example).
3275
3276=head2 exportsw
3277
3278 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY]
3279
3280This command print the content of the switch database. There is actually two format.
3281One is just TXT for terminal and the other is the DOT format from the graphviz environnement.
3282By default, format is TXT.
3283
3284 klask exportsw --format dot > /tmp/map.dot
3285 dot -Tpng /tmp/map.dot > /tmp/map.png
3286
3287In case you have too many switch connected on one switch,
3288the graphviz result graph could be too much vertical.
3289With C<--modulo> > 0, you can specify how many switches (connected on one switch) are on the same columns
3290before shifting them to one column to the left and back again.
3291The C<--shift> parameter must be 1, 2 or 3.
3292With C<--shift> egual to 2, the shift will be to two column to the left.
3293With 3, it will be 1 to the left and 2 to the left one time over two !
3294In practise, we just add virtuals nodes in the dot file,
3295that means the result graph is generated with theses virtuals but invisibles nodes...
3296
3297=head2 ip-free
3298
3299 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
3300
3301This command return IP address that was not use (detected by Klask) at this time.
3302The list returned could be limited to just one VLAN.
3303IP returned could have been never used or no computer have been detected since the number of days specified
3304(2 years by default).
3305This parameter could also be define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
3306
3307 default:
3308   days-to-death: 730
3309
3310Computer that does not have the good IP but takes a float one (see L</cleandb>) are taken into account.
3311
3312
3313=head2 bad-vlan-id
3314
3315 klask bad-vlan-id [--day|-d days_before_alert]
3316
3317This command return a list of switch port that are not configure with the good VLAN.
3318Computer which are in bad VLAN are detected with the float regex parameter (see L</cleandb>)
3319and another prior trace where they had the good IP (good DNS name).
3320The computer must stay connected on a bad VLAN more than XX days (15 days by default) before alert.
3321This parameter could also define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
3322
3323 default:
3324   days-before-alert: 15
3325
3326This functionality is not need if your switch use RADIUS 802.1X configuration...
3327
3328
3329=head2 poe-enable
3330
3331 klask poe-enable  [--verbose|-v] switch port
3332
3333This command activate the PoE (Power over Ethernet) on a switch port by SNMP.
3334So you need to give the switch name and a port on the command line.
3335See L</ABBREVIATION FOR PORT>.
3336
3337Warning: Only NEXANS switches are supported (we do not have other switch for testing).
3338You need to have the SNMP write access on the switch in order to modify it's configuration.
3339
3340
3341=head2 poe-disable
3342
3343 klask poe-disable [--verbose|-v] switch port
3344
3345This command deactivate the PoE (Power over Ethernet) on a switch port by SNMP.
3346So you need to give the switch name and a port on the command line.
3347See L</ABBREVIATION FOR PORT>.
3348
3349Warning: Only NEXANS switches are supported (we do not have other switch for testing).
3350You need to have the SNMP write access on the switch in order to modify it's configuration.
3351
3352
3353=head2 poe-status
3354
3355 klask poe-status  [--verbose|-v] switch port
3356
3357This command return the status of the PoE (Power over Ethernet) on a switch port by SNMP.
3358The return value could be C<enable> or C<disable> word.
3359So you need to give the switch name and a port on the command line.
3360See L</ABBREVIATION FOR PORT>.
3361
3362If it's not possible to change the PoE status with command L</poe-enable> and L</poe-disable>
3363(SNMP community read write access),
3364it's always possible to have the PoE port status.
3365
3366Warning: Only NEXANS switches are supported (we do not have other switch for testing).
3367
3368
3369=head1 CONFIGURATION
3370
3371Because Klask need many parameters, it's not possible actually to use command line parameters for everything.
3372The configuration is done in a F</etc/klask/klask.conf> YAML file.
3373This format have many advantage over XML, it's easier to read and to write !
3374
3375Here an example, be aware with indent, it's important in YAML, do not use tabulation !
3376
3377 default:
3378   community: public
3379   community-rw: private
3380   snmpport: 161
3381   float-regex: '(?^msx: ^float )'
3382   scan-mode: active
3383
3384 network:
3385   labnet:
3386     ip-subnet:
3387       - add: 192.168.1.0/24
3388       - add: 192.168.2.0/24
3389     interface: eth0
3390     vlan-id: 12
3391     main-router: gw1.labnet.local
3392
3393   schoolnet:
3394     ip-subnet:
3395       - add: 192.168.3.0/24
3396       - add: 192.168.4.0/24
3397     interface: eth0.38
3398     vlan-id: 13
3399     main-router: gw2.schoolnet.local
3400     scan-mode: passive
3401
3402   etunet:
3403     ip-subnet:
3404       - add: 192.168.5.0/24
3405     interface: eth2
3406     vlan-id: 14
3407     main-router: gw3.etunet.local
3408     scan-mode: passive
3409
3410 switch:
3411   - hostname: sw1.klask.local
3412     location: BatY / 1 floor / K004
3413     portignore:
3414       - 1
3415       - 2
3416
3417   - hostname: sw2.klask.local
3418     location: BatY / 2 floor / K203
3419     type: HP2424
3420     portignore:
3421       - 1
3422       - 2
3423     fake-ip: 192.168.9.14
3424
3425   - hostname: sw3.klask.local
3426     location: BatY / 2 floor / K203
3427
3428I think it's pretty easy to understand.
3429The default section can be overide in any section, if parameter mean something in theses sections.
3430Network to be scan are define in the network section. You must put an add by network.
3431Maybe I will make a delete line to suppress specific computers.
3432The switch section define your switch.
3433You have to write the port number to ignore, this was important if your switchs are cascades
3434(right now, method C<updatesw> find them automatically)
3435and is still important if you have loop (with PVST or MSTP).
3436Just put the ports numbers between switch.
3437
3438The C<community> parameter is use to get SNMP data on switch.
3439It could be overload for each switch.
3440By default, it's value is C<public> and you have to configure a readonly word for safety reason.
3441Some few command change the switch state as the commands L</enable> and L</disable>.
3442In theses rares cases, you need a readwrite SNMP community word define in your configuration file.
3443Klask then use since version C<0.6.2> the C<community-rw> parameter which by default is egal to C<private>.
3444
3445
3446=head1 ABBREVIATION FOR PORT
3447
3448HP Procurve and Nexans switches have a simplistic numbering scheme.
3449It's just number: 1, 2, 3... 24.
3450On HP8000 chassis, ports names begin with an uppercase letter: A1, A2...
3451Nothing is done on theses ports names.
3452
3453On HP Comware and DELL, port digitization schema use a port speed word (generally a very verbose word)
3454followed by tree number.
3455In order to have short name,
3456we made the following rules:
3457
3458 Bridge-Aggregation     -> Br
3459 FastEthernet           -> Fa
3460 Forty-GigabitEthernet  -> Fo
3461 FortyGigabitEthernet   -> Fo
3462 GigabitEthernet        -> Gi
3463 Giga                   -> Gi
3464 Port-Channel           -> Po
3465 Ten-GigabitEthernet    -> Te
3466 TenGigabitEthernet     -> Te
3467 Ten                    -> Te
3468
3469All Klask command automatically normalize the port name on standart output
3470and also on input command line.
3471
3472In the case of use an aggregator port (Po, Tk, Br ...),
3473the real ports used are also return.
3474
3475
3476=head1 SWITCH SUPPORTED
3477
3478Here is a list of switches where Klask gives or gave (for old switches) good results.
3479We have only a few manageable switches to actually test Klask.
3480It is quite possible that switches from other brands will work just as well.
3481You just have to do a test on it and add the line of description that goes well in the source code.
3482Contact us for any additional information.
3483
3484In the following list, the names of the switch types written in parentheses are the code names returned by Klask.
3485This makes it possible to adjust the code names of the different manufacturers!
3486
3487HP: J3299A(HP224M), J4120A(HP1600M), J9029A(HP1800-8G), J9449A(HP1810-8G), J4093A(HP2424M), J9279A(HP2510G-24),
3488J9280A(HP2510G-48), J4813A(HP2524), J4900A(HP2626A), J4900B(HP2626B), J4899B(HP2650), J9021A(HP2810-24G), J9022A(HP2810-48G),
3489J8692A(HP3500-24G), J4903A(HP2824), J4110A(HP8000M), JE074A(HP5120-24G), JE069A(HP5120-48G), JD377A(HP5500-24G), JD374A(HP5500-24F).
3490
3491BayStack: BayStack 350T HW(BS350T)
3492
3493Nexans: GigaSwitch V3 TP SFP-I 48V ES3(NA3483-6G), GigaSwitch V3 TP.PSE.+ 48/54V ES3(NA3483-6P)
3494
3495DELL: PC7024(DPC7024), N2048(DN2048), N4032F(DN4032F), N4064F(DN4064F)
3496
3497H3C and 3COM switches have never not been tested but the new HP Comware switches are exactly the same...
3498
3499H3C: H3C5500
3500
35013COM: 3C17203, 3C17204, 3CR17562-91, 3CR17255-91, 3CR17251-91, 3CR17571-91, 3CRWX220095A, 3CR17254-91, 3CRS48G-24S-91,
35023CRS48G-48S-91, 3C17708, 3C17709, 3C17707, 3CR17258-91, 3CR17181-91, 3CR17252-91, 3CR17253-91, 3CR17250-91, 3CR17561-91,
35033CR17572-91, 3C17702-US, 3C17700.
3504
3505
3506=head1 FILES
3507
3508 /etc/klask/klask.conf
3509 /var/lib/klask/klaskdb
3510 /var/lib/klask/switchdb
3511
3512
3513=head1 SEE ALSO
3514
3515Net::SNMP, Net::Netmask, Net::CIDR::Lite, NetAddr::IP, YAML
3516
3517=over
3518
3519=item * L<Web site|http://servforge.legi.grenoble-inp.fr/projects/klask>
3520
3521=item * L<Online Manual|http://servforge.legi.grenoble-inp.fr/pub/klask/klask.html>
3522
3523=back
3524
3525
3526=head1 VERSION
3527
3528$Id: klask 312 2017-10-21 16:59:05Z g7moreau $
3529
3530
3531=head1 AUTHOR
3532
3533Written by Gabriel Moreau, Grenoble - France
3534
3535
3536=head1 LICENSE AND COPYRIGHT
3537
3538GPL version 2 or later and Perl equivalent
3539
3540Copyright (C) 2005-2017 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>.
Note: See TracBrowser for help on using the repository browser.