source: trunk/klask @ 373

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