source: trunk/klask @ 311

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