source: trunk/klask @ 349

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