source: trunk/klask @ 411

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