source: trunk/klask @ 321

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