source: trunk/klask @ 364

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