source: trunk/klask

Last change on this file was 412, checked in by g7moreau, 4 years ago
  • rename TMP to CACHE_FOLDER. User can change it inside the configuration file (update sample)
  • Property svn:executable set to *
  • Property svn:keywords set to Date Author Id Rev
File size: 145.4 KB
RevLine 
[2]1#!/usr/bin/perl -w
[32]2#
[408]3# Copyright (C) 2005-2020 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>
[372]4# License GNU GPL version 2 or later and Perl equivalent
[32]5#
6# $Id: klask 412 2020-01-28 15:56:40Z g7moreau $
[369]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
[405]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
[2]13
14use strict;
[3]15use warnings;
[412]16use version; our $VERSION = version->declare('0.7.14');
[2]17
[63]18use Readonly;
[64]19use FileHandle;
[2]20use Net::SNMP;
[44]21#use YAML;
22use YAML::Syck;
[2]23use Net::Netmask;
24use Net::CIDR::Lite;
25use NetAddr::IP;
[110]26use Getopt::Long qw(GetOptions);
[68]27use Socket;
[109]28use List::Util 'shuffle';
[295]29use Digest::SHA qw(sha512_base64);
[311]30use Text::Table; # libtext-table-perl http://blogs.perl.org/users/steven_haryanto/2014/07/benchmarking-several-ascii-table-generator-modules.html
[2]31
[218]32################################################################
33# general initialization
34################################################################
35
[100]36my $KLASK_VAR      = '/var/lib/klask';
[92]37my $KLASK_CFG_FILE = '/etc/klask/klask.conf';
[28]38my $KLASK_DB_FILE  = "$KLASK_VAR/klaskdb";
39my $KLASK_SW_FILE  = "$KLASK_VAR/switchdb";
[2]40
[26]41test_running_environnement();
42
[44]43my $KLASK_CFG = YAML::Syck::LoadFile("$KLASK_CFG_FILE");
[2]44
[221]45my %DEFAULT     = %{$KLASK_CFG->{'default'}};
46my @SWITCH_LIST = @{$KLASK_CFG->{'switch'}};
[2]47
[196]48my %SWITCH_LEVEL = ();
[63]49my %SWITCH_DB    = ();
[2]50LEVEL_OF_EACH_SWITCH:
[209]51for my $sw (@SWITCH_LIST) {
[369]52   $SWITCH_LEVEL{$sw->{'hostname'}} = $sw->{'level'} || $DEFAULT{'switch_level'} || 2;
[261]53   $SWITCH_DB{$sw->{'hostname'}} = $sw;
[208]54
55   # SNMP parameter initialisation
[370]56   my %session = (-hostname => $sw->{'hostname'});
[221]57   $session{-version} = $sw->{'version'}  || 1;
[370]58   $session{-port}    = $sw->{'snmpport'} || $DEFAULT{'snmpport'} || 161;
[221]59   if (exists $sw->{'version'} and $sw->{'version'} eq '3') {
60      $session{-username} = $sw->{'username'} || 'snmpadmin';
[208]61      }
62   else {
[221]63      $session{-community} = $sw->{'community'} || $DEFAULT{'community'} || 'public';
[208]64      }
[221]65   $sw->{'snmp_param_session'} = \%session;
[2]66   }
[369]67@SWITCH_LIST = reverse sort {$SWITCH_LEVEL{$a->{'hostname'}} <=> $SWITCH_LEVEL{$b->{'hostname'}}} @{$KLASK_CFG->{'switch'}};
[2]68
[196]69#my %SWITCH_PORT_COUNT = ();
[2]70
[11]71my %CMD_DB = (
[133]72   'help'                 => \&cmd_help,
73   'version'              => \&cmd_version,
74   'exportdb'             => \&cmd_exportdb,
75   'updatedb'             => \&cmd_updatedb,
76   'searchdb'             => \&cmd_searchdb,
77   'removedb'             => \&cmd_removedb,
[389]78   'insertdb'             => \&cmd_insertdb,
[133]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,
[35]88   'search-mac-on-switch' => \&cmd_search_mac_on_switch,
[133]89   'bad-vlan-id'          => \&cmd_bad_vlan_id,
[239]90   'poe-enable'           => \&cmd_poe_enable,
91   'poe-disable'          => \&cmd_poe_disable,
92   'poe-status'           => \&cmd_poe_status,
[238]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,
[305]98   'host-setlocation'     => \&cmd_host_setlocation,
[142]99   'rebootsw'             => \&cmd_rebootsw,
[2]100   );
101
[218]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;
[2]113
[63]114Readonly my %SWITCH_KIND => (
[128]115   # HP
[364]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},               },
[395]127   J4900B           => { type => 1, model => 'HP2626B',        match => 'J4900B.+?Switch 2626',                revision => qr{, revision (\w[\d\.]+?), ROM},               },
[364]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},               },
[308]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},   },
[406]140   JH149A           => { type => 2, model => 'HP5510-24F',     match => 'HPE Comware.+?HPE 5510 .+? JH149A',   revision => qr{Comware .*? Version (\d[\d\.]+?), Release},  },
[128]141   # BayStack
[171]142   BS350T           => { type => 1, model => 'BS350T',         match => 'BayStack 350T HW'                     },
[366]143   # Microsens
144   'MS440209PM-48G6'=> { type => 2, model => 'MS440209PM-48G6+',  match => 'MICROSENS G6 Switch'               },
[128]145   # Nexans
[407]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+?)\)},                 },
[145]150   # DELL
[308]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},   },
[128]155   # 3COM
[171]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'                     },
[18]179   );
[63]180
181Readonly my %OID_NUMBER => (
[68]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',
[369]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
[19]198   );
[18]199
[206]200Readonly my %PORT_UPDOWN => (
201   1 => 'enable',
202   2 => 'disable',
203   );
204
[63]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
[223]208Readonly my $RE_FLOAT_HOSTNAME => $DEFAULT{'float-regex'} || qr{ ^float }xms;
[63]209
[369]210Readonly my $SEP_AGGREGATOR_PORT => ','; # : is already use to join switch and port
[248]211Readonly my $SEP_SWITCH_PORT     => ':';
[246]212
213
[218]214################################################################
[206]215# main program
[218]216################################################################
[2]217
[11]218my $cmd = shift @ARGV || 'help';
219if (defined $CMD_DB{$cmd}) {
220   $CMD_DB{$cmd}->(@ARGV);
[2]221   }
222else {
[63]223   print {*STDERR} "klask: command $cmd not found\n\n";
[310]224   $CMD_DB{'help'}->();
[2]225   exit 1;
226   }
227
228exit;
229
[218]230################################################################
[206]231# subroutine
[218]232################################################################
[206]233
[218]234#---------------------------------------------------------------
[26]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";
[63]238   return;
[26]239   }
240
[218]241#---------------------------------------------------------------
[34]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";
[63]244   return;
[34]245   }
246
[218]247#---------------------------------------------------------------
[34]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";
[63]250   return;
[34]251   }
252
[218]253#---------------------------------------------------------------
[2]254# fast ping dont l'objectif est de remplir la table arp de la machine
[111]255sub fast_ping {
[68]256   # Launch this command without waiting...
[258]257   system "fping -q -c 1 @_ > /dev/null 2>&1 &";
[63]258   return;
[2]259   }
260
[218]261#---------------------------------------------------------------
[63]262sub shell_command {
263   my $cmd = shift;
264
[64]265   my $fh     = new FileHandle;
266   my $result = '';
[96]267   open $fh, q{-|}, "LANG=C $cmd" or die "Can't exec $cmd\n";
[64]268   $result .= <$fh>;
[63]269   close $fh;
270   chomp $result;
271   return $result;
272   }
273
[218]274#---------------------------------------------------------------
[2]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;
[369]278   my $interface        = shift || q{*};
279   my $type             = shift || q{fast};
280   my $already          = shift || q{yes};
[2]281
282   my %ret = (
283      hostname_fq  => 'unknow',
284      ipv4_address => '0.0.0.0',
285      mac_address  => 'unknow',
286      );
287
[68]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;
[261]291   $ret{'ipv4_address'} = inet_ntoa($packed_ip);
292   #if ($ret{'ipv4_address'} !~ m/$RE_IPv4_ADDRESS/) {
[389]293   #   print "Error: for computer $param_ip_or_host on interface $interface, IPv4 $ret{'ipv4_address'} is not valide\n";
[197]294   #   return %ret;
295   #   }
[68]296
297   # perl -MSocket -E 'say scalar gethostbyaddr(inet_aton("194.254.66.240"), AF_INET)'
[186]298   my $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET) if $already eq 'yes';
[261]299   $ret{'hostname_fq'} = $hostname_fq if defined $hostname_fq;
[68]300
[404]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';
[62]305   my $cmd_arpwatch = shell_command $cmd;
[144]306   my ($interface2, $arp, $ip, $timestamp, $host) = split m/ \s+ /xms, $cmd_arpwatch;
[44]307
[261]308   $ret{'interface'}    = $interface2 || $interface;
309   $ret{'mac_address'}  = $arp       if $arp;
310   $ret{'timestamp'}    = $timestamp if $timestamp;
[2]311
[63]312   my $nowtimestamp = time;
[3]313
[369]314   if ($type eq 'fast' and (not defined $timestamp or $timestamp < ($nowtimestamp - 45 * 60))) { # 45 min
[261]315      $ret{'mac_address'} = 'unknow';
[3]316      return %ret;
317      }
318
[194]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
[197]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
[370]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   #      }
[261]332   if ($ret{'mac_address'} eq 'unknow') {
[196]333      # Last chance to have the mac_address
[261]334      if ($ret{'interface'} eq '*') {
[369]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) {
[261]337            ($ret{'interface'}, $ret{'mac_address'}) = ($1, $2);
[197]338            }
[194]339         }
[197]340      else {
[369]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) {
[261]343            $ret{'mac_address'} = $1;
[197]344            }
345         }
[63]346      }
[2]347
[96]348   # Normalize MAC Address
[261]349   if ($ret{'mac_address'} ne 'unknow') {
[2]350      my @paquets = ();
[369]351      for (split m/ : /xms, $ret{'mac_address'}) {
[63]352         my @chars = split m//xms, uc "00$_";
[2]353         push @paquets, "$chars[-2]$chars[-1]";
354         }
[261]355      $ret{'mac_address'} = join q{:}, @paquets;
[2]356      }
357
358   return %ret;
359   }
360
[246]361#---------------------------------------------------------------
[20]362# Find Surname of a switch
363sub get_switch_model {
[22]364   my $sw_snmp_description = shift || 'unknow';
[169]365   $sw_snmp_description =~ s/[\n\r]/ /g;
[63]366
[20]367   for my $sw_kind (keys %SWITCH_KIND) {
[310]368      next if not $sw_snmp_description =~ m/$SWITCH_KIND{$sw_kind}->{'match'}/ms; # option xms break search, why ?
[63]369
[309]370      return $SWITCH_KIND{$sw_kind}->{'model'};
[20]371      }
[63]372
[22]373   return $sw_snmp_description;
[20]374   }
375
[218]376#---------------------------------------------------------------
[308]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) {
[310]383      next if not $sw_snmp_description =~ m/$SWITCH_KIND{$sw_kind}->{'match'}/ms; # option xms break search, why ?
[309]384      last if not exists $SWITCH_KIND{$sw_kind}->{'revision'};
[308]385
[309]386      my ($revision) = $sw_snmp_description =~ m/$SWITCH_KIND{$sw_kind}->{'revision'}/xms;
[308]387      return $revision || 'unknow';
388      }
389
390   return 'unknow';
391   }
392
393#---------------------------------------------------------------
[304]394# Get switch name and switch model
[4]395sub init_switch_names {
[173]396   my ($verbose, $verb_description, $check_hostname, $check_location) = @_;
[63]397
[369]398   printf "%-26s                %-25s %s\n", 'Switch', 'Description', 'Type(Revision)' if $verbose;
[308]399   print "----------------------------------------------------------------------------------\n" if $verbose;
[2]400
401   INIT_EACH_SWITCH:
[293]402   for my $sw (my @CLONE = @SWITCH_LIST) { # Make a local clone because some element can be deleted
[369]403      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
[14]404      print "$error \n" if $error;
405
[2]406      my $result = $session->get_request(
[18]407         -varbindlist => [
[261]408            $OID_NUMBER{'sysDescription'},
409            $OID_NUMBER{'sysName'},
410            $OID_NUMBER{'sysContact'},
411            $OID_NUMBER{'sysLocation'},
[18]412            ]
[2]413         );
[196]414      if (!defined $result) {
415         printf {*STDERR} "ERROR: %s.\n", $session->error();
416         $session->close();
417         # Remove bad switch
[369]418         @SWITCH_LIST = grep {$_->{'hostname'} ne $sw->{'hostname'}} @SWITCH_LIST;
[261]419         delete $SWITCH_LEVEL{$sw->{'hostname'}} if exists $SWITCH_LEVEL{$sw->{'hostname'}};
420         delete $SWITCH_DB{$sw->{'hostname'}}    if exists $SWITCH_DB{$sw->{'hostname'}};
[196]421         next INIT_EACH_SWITCH;
422         }
423
[262]424      $sw->{'description'} = $result->{$OID_NUMBER{'sysName'}} || $sw->{'hostname'};
[370]425      $sw->{'model'}       = get_switch_model($result->{$OID_NUMBER{'sysDescription'}});
426      $sw->{'revision'}    = get_switch_revision($result->{$OID_NUMBER{'sysDescription'}});
[309]427      printf "%-26s 0--------->>>> %-25s %s\n", $sw->{'hostname'}, $sw->{'description'}, $sw->{'model'}.'('.$sw->{'revision'}.')' if $verbose;
[304]428
[170]429      if ($verb_description) {
[261]430         my $desc = $result->{$OID_NUMBER{'sysDescription'}};
[170]431         $desc =~ s/[\n\r]/ /g;
[308]432         print " +> $sw->{'hostname'} - description: $desc\n";
[170]433         }
[172]434      if ($check_hostname) {
[261]435         my ($hostname) = split /\./, $sw->{'hostname'}, 2;
[304]436         print " +> $hostname - error internal hostname: $sw->{'hostname'}\n" if $result->{$OID_NUMBER{'sysName'}} ne $hostname;
[172]437         }
[173]438      if ($check_location) {
[261]439         my $location = $result->{$OID_NUMBER{'sysLocation'}};
[174]440         $location =~ s/^"(.+)"$/$1/;
[304]441         print " +> $sw->{'hostname'} - error location: '$location' -> '$sw->{'location'}'\n" if $location ne $sw->{'location'};
[173]442         }
[304]443
[2]444      $session->close;
445      }
446
447   print "\n" if $verbose;
[63]448   return;
[2]449   }
450
[218]451#---------------------------------------------------------------
[246]452# convert hexa (only 2 digits) to decimal
453sub digit_hex2dec {
[2]454   #00:0F:1F:43:E4:2B
[63]455   my $car = '00' . uc shift;
[2]456
[4]457   return '00' if $car eq '00UNKNOW';
[2]458   my %table = (
[62]459      '0'=>'0',  '1'=>'1',  '2'=>'2',  '3'=>'3',  '4'=>'4',
460      '5'=>'5',  '6'=>'6',  '7'=>'7',  '8'=>'8',  '9'=>'9',
[63]461      'A'=>'10', 'B'=>'11', 'C'=>'12', 'D'=>'13', 'E'=>'14', 'F'=>'15',
[2]462      );
[63]463   my @chars = split m//xms, $car;
[370]464   return $table{$chars[-2]} * 16 + $table{$chars[-1]};
[2]465   }
466
[218]467#---------------------------------------------------------------
[136]468
469sub normalize_mac_address {
470   my $mac_address = shift;
471
[228]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      ) {
[229]476      $mac_address =~ s/[-\.]//g;
[139]477      return join q{:}, unpack('(A2)*', uc($mac_address));
478      }
479
[370]480   return join q{:}, map {substr(uc("00$_"), -2)} split m/ [:-] /xms, $mac_address;
[136]481   }
482
[218]483#---------------------------------------------------------------
[246]484# convert MAC hex address to decimal
485sub mac_address_hex2dec {
[2]486   #00:0F:1F:43:E4:2B
[86]487   my $mac_address = shift;
[2]488
[86]489   my @paquets = split m/ : /xms, $mac_address;
[63]490   my $return = q{};
[165]491   for (@paquets) {
[246]492      $return .= q{.} . digit_hex2dec($_);
[2]493      }
494   return $return;
495   }
496
[218]497#---------------------------------------------------------------
[250]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
[317]509   $port_hr =~ s/($SEP_AGGREGATOR_PORT)/ /g;  # Other occurence
[250]510   return $port_hr;
511   }
512
513#---------------------------------------------------------------
[348]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#---------------------------------------------------------------
[317]541# fqdn_html_breakable change it's parameter, use it like chomp perl function
542
543sub fqdn_html_breakable {
544   my $ref_fqdn = \($_[0]);
[370]545
[317]546   # Add HTML <wbr> before every . to permit line break
[318]547   $$ref_fqdn =~ s{\.}{.<wbr />}g;
[317]548   }
549
550#---------------------------------------------------------------
[246]551# return the port and the switch where the computer is connected
[2]552sub find_switch_port {
[86]553   my $mac_address     = shift;
[63]554   my $switch_proposal = shift || q{};
[370]555   my $vlan_id         = shift || 0;
[63]556
[2]557   my %ret;
[261]558   $ret{'switch_description'} = 'unknow';
[370]559   $ret{'switch_port_id'}     = '0';
[2]560
[370]561   return %ret if $mac_address eq 'unknow';
[2]562
[196]563   my @switch_search = @SWITCH_LIST;
[63]564   if ($switch_proposal ne q{}) {
[196]565      for my $sw (@SWITCH_LIST) {
[261]566         next if $sw->{'hostname'} ne $switch_proposal;
[22]567         unshift @switch_search, $sw;
[2]568         last;
569         }
570      }
571
[277]572   my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
[370]573   my $oid_search_port2 = $OID_NUMBER{'searchPort2'} . '.' . $vlan_id . mac_address_hex2dec($mac_address);
[63]574
[2]575   LOOP_ON_SWITCH:
[22]576   for my $sw (@switch_search) {
[370]577      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
[22]578      print "$error \n" if $error;
579
[2]580      my $result = $session->get_request(
[277]581         -varbindlist => [$oid_search_port1]
[2]582         );
[124]583      if (not defined $result) {
584         $result = $session->get_request(
[277]585            -varbindlist => [$oid_search_port2]
[124]586            );
[277]587         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
[124]588         }
589
[370]590      if (not(defined $result and $result->{$oid_search_port1} ne 'noSuchInstance')) {
[2]591         $session->close;
592         next LOOP_ON_SWITCH;
593         }
594
[277]595      my $swport_id = $result->{$oid_search_port1};
[271]596      my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);
[2]597
[150]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:
[310]607      for my $portignore (@{$sw->{'portignore'}}) {
[167]608         next LOOP_ON_SWITCH if $swport_hr eq $portignore;
[246]609         my ($swport_hr_limited) = split /$SEP_AGGREGATOR_PORT/, $swport_hr; # Beginning of the swith port (Aggregator)
[241]610         next LOOP_ON_SWITCH if $swport_hr_limited eq $portignore;
[150]611         }
612
[261]613      $ret{'switch_hostname'}    = $sw->{'hostname'};
[262]614      $ret{'switch_description'} = $sw->{'description'};
[271]615      $ret{'switch_port_id'}     = $swport_id;
[261]616      $ret{'switch_port_hr'}     = $swport_hr; # human readable
[150]617
618      last LOOP_ON_SWITCH;
[2]619      }
620   return %ret;
621   }
622
[218]623#---------------------------------------------------------------
[246]624# search all the port on all the switches where the computer is detected
[2]625sub find_all_switch_port {
[86]626   my $mac_address = shift;
[144]627   my $vlan_id     = shift || 0;
[2]628
629   my $ret = {};
630
[86]631   return $ret if $mac_address eq 'unknow';
[2]632
[277]633   my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
[370]634   my $oid_search_port2 = $OID_NUMBER{'searchPort2'} . '.' . $vlan_id . mac_address_hex2dec($mac_address);
[2]635   LOOP_ON_ALL_SWITCH:
[196]636   for my $sw (@SWITCH_LIST) {
[221]637      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
[13]638      print "$error \n" if $error;
639
[2]640      my $result = $session->get_request(
[277]641         -varbindlist => [$oid_search_port1]
[2]642         );
[124]643      if (not defined $result) {
644         $result = $session->get_request(
[277]645            -varbindlist => [$oid_search_port2]
[124]646            );
[277]647         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
[124]648         }
[13]649
[277]650      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
651         my $swport_id = $result->{$oid_search_port1};
[271]652         my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);
[2]653
[181]654         SWITCH_PORT_IGNORE:
[310]655         for my $portignore (@{$sw->{'portignore'}}) {
[181]656            if ($swport_hr eq $portignore) {
657               $session->close;
[370]658               next LOOP_ON_ALL_SWITCH;
[181]659               }
660            }
661
[404]662         $ret->{$sw->{'hostname'}}                = {};
[261]663         $ret->{$sw->{'hostname'}}{'hostname'}    = $sw->{'hostname'};
[262]664         $ret->{$sw->{'hostname'}}{'description'} = $sw->{'description'};
[272]665         $ret->{$sw->{'hostname'}}{'port_id'}     = $swport_id;
[261]666         $ret->{$sw->{'hostname'}}{'port_hr'}     = $swport_hr;
[2]667         }
668
669      $session->close;
670      }
671   return $ret;
672   }
673
[218]674#---------------------------------------------------------------
[2]675sub get_list_network {
676
[262]677   return keys %{$KLASK_CFG->{'network'}};
[2]678   }
679
[218]680#---------------------------------------------------------------
[2]681sub get_current_interface {
[113]682   my $vlan_name = shift;
[2]683
[262]684   return $KLASK_CFG->{'network'}{$vlan_name}{'interface'};
[2]685   }
[63]686
[218]687#---------------------------------------------------------------
[144]688sub get_current_vlan_id {
689   my $vlan_name = shift;
690
[262]691   return 0 if not exists $KLASK_CFG->{'network'}{$vlan_name};
692   return $KLASK_CFG->{'network'}{$vlan_name}{'vlan-id'};
[144]693   }
694
[218]695#---------------------------------------------------------------
[179]696sub get_current_scan_mode {
697   my $vlan_name = shift;
698
[262]699   return $KLASK_CFG->{'network'}{$vlan_name}{'scan-mode'} || $DEFAULT{'scan-mode'} || 'active';
[179]700   }
701
[218]702#---------------------------------------------------------------
[144]703sub get_current_vlan_name_for_interface {
704   my $interface = shift;
705
[262]706   for my $vlan_name (keys %{$KLASK_CFG->{'network'}}) {
707      next if $KLASK_CFG->{'network'}{$vlan_name}{'interface'} ne $interface;
[144]708      return $vlan_name;
709      }
710   }
711
[218]712#---------------------------------------------------------------
[2]713# liste l'ensemble des adresses ip d'un réseau
714sub get_list_ip {
[113]715   my @vlan_name = @_;
[2]716
717   my $cidrlist = Net::CIDR::Lite->new;
718
[113]719   for my $net (@vlan_name) {
[370]720      my @line = @{$KLASK_CFG->{'network'}{$net}{'ip-subnet'}};
[2]721      for my $cmd (@line) {
[209]722         for my $method (keys %{$cmd}) {
[2]723            $cidrlist->add_any($cmd->{$method}) if $method eq 'add';
724            }
725         }
726      }
727
[4]728   my @res = ();
[2]729
730   for my $cidr ($cidrlist->list()) {
731      my $net = new NetAddr::IP $cidr;
[63]732      for my $ip (@{$net}) {
733         $ip =~ s{ /32 }{}xms;
[370]734         push @res, $ip;
[2]735         }
736      }
737
738   return @res;
739   }
740
[218]741#---------------------------------------------------------------
[9]742# liste l'ensemble des routeurs du réseau
743sub get_list_main_router {
[113]744   my @vlan_name = @_;
[9]745
746   my @res = ();
747
[113]748   for my $net (@vlan_name) {
[262]749      push @res, $KLASK_CFG->{'network'}{$net}{'main-router'};
[9]750      }
751
752   return @res;
753   }
754
[218]755#---------------------------------------------------------------
[214]756sub normalize_port_human_readable {
[370]757   my $sw_port_hr = shift;
[150]758
[214]759   # Manufacturer abbreviation
[162]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;
[150]766
[214]767   # Customer abbreviation
768   $sw_port_hr =~ s/^Ten/Te/i;
769   $sw_port_hr =~ s/^Giga/Gi/i;
770
[215]771   return ucfirst $sw_port_hr;
[150]772   }
773
[218]774#---------------------------------------------------------------
[220]775sub snmp_get_rwsession {
776   my ($sw) = @_;
777
[221]778   my %session = %{$sw->{'snmp_param_session'}};
779   $session{-community} = $sw->{'community-rw'} || $DEFAULT{'community-rw'} || 'private';
[220]780   return %session;
781   }
782
783#---------------------------------------------------------------
[271]784sub snmp_get_switchport_id2hr {
785   my ($snmp_session, $swport_id) = @_;
[198]786
[271]787   # On H3C, port id (port_id) and port index (port_ix) are not the same
[242]788   # Double SNMP request to get the name
[271]789   # First get the index, second get the name (port_hr)
[244]790
[370]791   my $oid_search_ix = $OID_NUMBER{'ifIndex'} . '.' . $swport_id;
[271]792   my $result_ix = $snmp_session->get_request(
[277]793      -varbindlist => [$oid_search_ix]
[244]794      );
795
[271]796   my $swport_ix = $swport_id;
[277]797   $swport_ix = $result_ix->{$oid_search_ix} if defined $result_ix;
[157]798
[271]799   return snmp_get_switchport_ix2hr($snmp_session, $swport_ix);
[244]800   }
[242]801
[244]802#---------------------------------------------------------------
[271]803sub snmp_get_switchport_ix2hr {
804   my ($snmp_session, $swport_ix) = @_;
[244]805
[277]806   my $oid_search_hr = $OID_NUMBER{'ifName'} .'.'. $swport_ix;
[157]807   my $result_hr = $snmp_session->get_request(
[277]808      -varbindlist => [$oid_search_hr]
[154]809      );
[271]810   my $swport_hr = $swport_ix;
[277]811   $swport_hr = normalize_port_human_readable($result_hr->{$oid_search_hr}) if defined $result_hr;
[240]812
813   # Aggregator port
814   if ($swport_hr =~ m/^(Trk|Br|Po)/) {
[277]815      my $oid_search_index = $OID_NUMBER{'ifAggregator'}; # base OID
[370]816      my @args = (-varbindlist => [$oid_search_index]);
[240]817      LOOP_ON_OID_PORT:
[370]818      while (defined $snmp_session->get_next_request(@args)) {
[240]819         my ($oid_current) = $snmp_session->var_bind_names;
[370]820         last LOOP_ON_OID_PORT if not Net::SNMP::oid_base_match($oid_search_index, $oid_current);
[240]821
822         # IEEE8023-LAG-MIB::dot3adAggPortSelectedAggID.28 = INTEGER: 337
823         # IEEE8023-LAG-MIB::dot3adAggPortAttachedAggID.28 = INTEGER: 337
[242]824         my $port_aggregator_index = $snmp_session->var_bind_list->{$oid_current};
[271]825         my ($current_port_ix) = reverse split /\./, $oid_current; # last number
[240]826
827         # prepare next loop item
828         @args = (-varbindlist => [$oid_current]);
829
[242]830         next LOOP_ON_OID_PORT if $port_aggregator_index == 0;
[271]831         next LOOP_ON_OID_PORT if not $port_aggregator_index == $swport_ix;
[240]832
[271]833         my $current_port_name = snmp_get_switchport_ix2hr($snmp_session, $current_port_ix);
[246]834         $swport_hr .= "$SEP_AGGREGATOR_PORT$current_port_name";
[240]835         }
836      }
[154]837   return $swport_hr;
838   }
839
[218]840#---------------------------------------------------------------
[213]841# Reverse search port number
[271]842sub snmp_get_switchport_hr2id {
[213]843   my ($snmp_session, $swport_hr, $verbose) = @_;
844
[240]845   # Split for Aggregator port
[245]846   # Keep only the Aggregator part
[246]847   ($swport_hr) = split /$SEP_AGGREGATOR_PORT/, $swport_hr;
[240]848
[271]849   my $swport_id = $swport_hr;
[246]850   # direct return if already numeric (next loop is expensive) / old or simple switch
[271]851   return $swport_id if $swport_id =~ m/^\d+$/;
[213]852
[277]853   my $oid_search_ix = $OID_NUMBER{'ifIndex'}; # base OID
[370]854   my @args = (-varbindlist => [$oid_search_ix]);
[217]855   LOOP_ON_OID_PORT:
[370]856   while (defined $snmp_session->get_next_request(@args)) {
[213]857      my ($oid_current) = $snmp_session->var_bind_names;
[370]858      last LOOP_ON_OID_PORT if not Net::SNMP::oid_base_match($oid_search_ix, $oid_current);
[290]859
[213]860      my $port_ifIndex = $snmp_session->var_bind_list->{$oid_current};
[271]861      my ($port_ix) = reverse split /\./, $oid_current; # last number
[213]862      printf "PORT1: %s => %s\n", $oid_current, $port_ifIndex if $verbose;
863
[217]864      # prepare next loop item
[213]865      @args = (-varbindlist => [$oid_current]);
[290]866
[370]867      my $oid_search_ifName = $OID_NUMBER{'ifName'} . '.' . $port_ifIndex;
[277]868      my $result = $snmp_session->get_request(-varbindlist => [$oid_search_ifName]);
[217]869      next LOOP_ON_OID_PORT if not defined $result;
[290]870
[277]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;
[214]873      if ($current_port_hr eq $swport_hr) {
[271]874         print "PORT3: $current_port_hr <-> $port_ix\n" if $verbose;
[290]875
[217]876         # return port number ifIndex need by OID portUpDown
[271]877         $swport_id = $port_ifIndex; # other possible value could be $port_ix
[217]878         last LOOP_ON_OID_PORT;
[213]879         }
880      }
[271]881   return $swport_id;
[213]882   }
883
[218]884#---------------------------------------------------------------
[237]885# Get the list of all the VLAN define on a switch
886sub snmp_get_vlan_list {
887   my ($snmp_session, $verbose) = @_;
888
[275]889   my %vlandb = (); # Hash vlan number => vlan name
[237]890
[277]891   my $oid_search_index = $OID_NUMBER{'vlanName'}; # base OID
[370]892   my @args = (-varbindlist => [$oid_search_index]);
[237]893   LOOP_ON_VLAN:
[370]894   while (defined $snmp_session->get_next_request(@args)) {
[237]895      my ($oid_current) = $snmp_session->var_bind_names;
[277]896      last LOOP_ON_VLAN if not Net::SNMP::oid_base_match($oid_search_index, $oid_current);
[237]897
898      my $vlan_name = $snmp_session->var_bind_list->{$oid_current};
[275]899      my ($vlan_id) = reverse split /\./, $oid_current; # last number
[237]900      printf "VLAN: %s => %s\n", $oid_current, $vlan_name if $verbose;
901
[275]902      $vlandb{$vlan_id} = $vlan_name;
[237]903
904      # prepare next loop item
905      @args = (-varbindlist => [$oid_current]);
906      }
[275]907   return %vlandb;
[237]908   }
909
910#---------------------------------------------------------------
[151]911# Load computer database
[150]912sub computerdb_load {
913   my $computerdb = YAML::Syck::LoadFile("$KLASK_DB_FILE");
[151]914
915   LOOP_ON_IP_ADDRESS:
916   for my $ip (keys %{$computerdb}) {
917
[274]918      # Rename switch_port -> switch_port_id (2017/09/15)
[270]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'};
[274]921         $computerdb->{$ip}{'switch_port_id'} = 0 if $computerdb->{$ip}{'switch_port_id'} !~ m/^\d+$/; # force numeric
[270]922         }
923      delete $computerdb->{$ip}{'switch_port'} if exists $computerdb->{$ip}{'switch_port'};
924
[261]925      next LOOP_ON_IP_ADDRESS if exists $computerdb->{$ip}{'switch_port_hr'} and defined $computerdb->{$ip}{'switch_port_hr'};
[198]926
[270]927      $computerdb->{$ip}{'switch_port_hr'} = $computerdb->{$ip}{'switch_port_id'};
[151]928      }
929
[150]930   return $computerdb;
931   }
932
[296]933#---------------------------------------------------------------
[387]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#---------------------------------------------------------------
[296]947sub get_switchdb_checksum {
[372]948   my %switch_db = @_; # same as global %SWITCH_DB
[296]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'},
[309]954         $switch_db{$sw_name}->{'model'},
[391]955         $switch_db{$sw_name}->{'revision'},
[296]956         $switch_db{$sw_name}->{'hostname'},
957         "\n";
958      }
959
960   return sha512_base64($checksum_data);
961   }
962
[300]963#---------------------------------------------------------------
964sub update_switchdb {
965   my %args = (
966      verbose => 0,
967      @_);
968
[370]969   init_switch_names('yes'); # nomme les switchs
[300]970   print "\n";
971
972   my %where = ();
973   my %db_switch_output_port = ();
974   my %db_switch_ip_hostnamefq = ();
975
976   DETECT_ALL_ROUTER:
[370]977   for my $one_router (get_list_main_router(get_list_network())) {
[300]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:
[370]995      for my $switch_detected (keys %{$where{$ip_router}}) {
[300]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
[370]1008   my @list_all_switch  = ();
[300]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') {
[370]1025            $resol_arp{'mac_address'} = $resol_arp_alt{'mac_address'};
1026            $resol_arp{'interface'}   = $resol_arp_alt{'interface'};
[300]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) {
[370]1059      #   for my $ip (Net::Netmask::sort_by_ip_address(@list_switch_ipv4)) {
[300]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};
[370]1064      #      next ALL_SWITCH_IP_ADDRESS if not exists $SWITCH_PORT_COUNT{ $db_switch_ip_hostnamefq{$ip} };
[300]1065
1066      DETECTED_SWITCH:
[370]1067      for my $switch_detected (keys %{$where{$ip}}) {
[300]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
[370]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'};
[300]1078         print "VERBOSE_7: +++++\n" if $args{'verbose'};
1079         }
1080
1081      }
1082
[370]1083   my %db_switch_connected_on_port          = ();
[300]1084   my $maybe_more_than_one_switch_connected = 'yes';
[370]1085   my $cloop                                = 0;
[300]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
[370]1100      $maybe_more_than_one_switch_connected = 'no';
[300]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}};
[370]1111         print "VERBOSE_10: $swport -- " . $#sw_on_same_port . " -- @sw_on_same_port\n" if $args{'verbose'};
[300]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
[370]1132   my %db_switch_parent = ();
[300]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
[218]1186################################################################
1187# command
1188################################################################
[2]1189
[218]1190#---------------------------------------------------------------
[2]1191sub cmd_help {
1192
[370]1193   print <<'END';
[224]1194klask - port and search manager for switches, map management
[2]1195
[129]1196 klask version
[224]1197 klask help
[129]1198
[301]1199 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
[226]1200 klask exportdb [--format|-f txt|html]
[389]1201 klask removedb ipv4_addr* computer*
1202 klask insertdb --ip ipv4_addr --mac mac_addr --network vlan_name
[396]1203 klask cleandb  [--verbose|-v] [--day number_of_day] [--repair-dns]
[2]1204
[224]1205 klask updatesw [--verbose|-v]
[382]1206 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY] [--way all|desc|child|parent] [--no-header|-H]
[45]1207
[226]1208 klask searchdb [--kind|-k host|mac] computer [mac-address]
[2]1209 klask search   computer
[226]1210 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr
[2]1211
[226]1212 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
[69]1213
[330]1214 klask bad-vlan-id [--day|-d days_before_alert] [--format|-f txt|html]
[129]1215
[223]1216 klask enable  [--verbose|-v] switch port
1217 klask disable [--verbose|-v] switch port
1218 klask status  [--verbose|-v] switch port
[237]1219
[247]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
[238]1224 klask vlan-getname switch vlan-id
1225 klask vlan-list switch
[2]1226END
[63]1227   return;
[2]1228   }
1229
[218]1230#---------------------------------------------------------------
[36]1231sub cmd_version {
1232
[370]1233   print <<'END';
[224]1234klask - port and search manager for switches, map management
[408]1235Copyright (C) 2005-2020 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>
[372]1236License GNU GPL version 2 or later and Perl equivalent
[342]1237END
[36]1238
[342]1239   print "Version $VERSION\n\n";
[37]1240   print ' $Id: klask 412 2020-01-28 15:56:40Z g7moreau $'."\n";
[63]1241   return;
[36]1242   }
1243
[238]1244#---------------------------------------------------------------
[2]1245sub cmd_search {
1246   my @computer = @_;
[63]1247
[370]1248   init_switch_names(); # nomme les switchs
[111]1249   fast_ping(@computer);
[133]1250
1251   LOOP_ON_COMPUTER:
[2]1252   for my $clientname (@computer) {
[404]1253      my %resol_arp = resolve_ip_arp_host($clientname);                          # resolution arp
[261]1254      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{'interface'});
[144]1255      my $vlan_id   = get_current_vlan_id($vlan_name);
[370]1256      my %where     = find_switch_port($resol_arp{'mac_address'}, '', $vlan_id); # retrouve l'emplacement
[133]1257
[404]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';
[133]1262
[146]1263      printf '%-22s %2s %-30s %-15s %18s',
[261]1264         $where{'switch_hostname'},
1265         $where{'switch_port_hr'},
1266         $resol_arp{'hostname_fq'},
1267         $resol_arp{'ipv4_address'},
[370]1268         $resol_arp{'mac_address'} . "\n";
[2]1269      }
[63]1270   return;
[2]1271   }
1272
[218]1273#---------------------------------------------------------------
[2]1274sub cmd_searchdb {
[397]1275   local @ARGV = @_;
[136]1276
1277   my $kind;
1278
[138]1279   GetOptions(
[137]1280      'kind=s'   => \$kind,
[136]1281      );
1282
1283   my %possible_search = (
[370]1284      host => \&cmd_searchdb_host,
1285      mac  => \&cmd_searchdb_mac,
[136]1286      );
1287
[137]1288   $kind = 'host' if not defined $possible_search{$kind};
[136]1289
1290   $possible_search{$kind}->(@ARGV);
1291   return;
1292   }
1293
[218]1294#---------------------------------------------------------------
[136]1295sub cmd_searchdb_host {
[2]1296   my @computer = @_;
1297
[111]1298   fast_ping(@computer);
[151]1299   my $computerdb = computerdb_load();
[63]1300
[2]1301   LOOP_ON_COMPUTER:
1302   for my $clientname (@computer) {
[370]1303      my %resol_arp = resolve_ip_arp_host($clientname); # resolution arp
1304      my $ip        = $resol_arp{'ipv4_address'};
[63]1305
[2]1306      next LOOP_ON_COMPUTER unless exists $computerdb->{$ip};
[63]1307
[370]1308      my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $computerdb->{$ip}{'timestamp'};
[2]1309      $year += 1900;
1310      $mon++;
[63]1311      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
[2]1312
1313      printf "%-22s %2s %-30s %-15s %-18s %s\n",
[261]1314         $computerdb->{$ip}{'switch_hostname'},
1315         $computerdb->{$ip}{'switch_port_hr'},
1316         $computerdb->{$ip}{'hostname_fq'},
[2]1317         $ip,
[261]1318         $computerdb->{$ip}{'mac_address'},
[2]1319         $date;
1320      }
[63]1321   return;
[2]1322   }
1323
[218]1324#---------------------------------------------------------------
[136]1325sub cmd_searchdb_mac {
[370]1326   my @mac = map {normalize_mac_address($_)} @_;
[136]1327
[151]1328   my $computerdb = computerdb_load();
[136]1329
1330   LOOP_ON_MAC:
1331   for my $mac (@mac) {
1332      LOOP_ON_COMPUTER:
1333      for my $ip (keys %{$computerdb}) {
[261]1334         next LOOP_ON_COMPUTER if $mac ne $computerdb->{$ip}{'mac_address'};
[198]1335
[370]1336         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $computerdb->{$ip}{'timestamp'};
[136]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",
[261]1342            $computerdb->{$ip}{'switch_hostname'},
1343            $computerdb->{$ip}{'switch_port_hr'},
1344            $computerdb->{$ip}{'hostname_fq'},
[136]1345            $ip,
[261]1346            $computerdb->{$ip}{'mac_address'},
[136]1347            $date;
[138]1348         #next LOOP_ON_MAC;
[136]1349         }
1350
1351      }
1352   return;
1353   }
1354
[218]1355#---------------------------------------------------------------
[2]1356sub cmd_updatedb {
[396]1357   local @ARGV = @_;
[170]1358
[301]1359   my ($verbose, $verb_description, $check_hostname, $check_location, $no_rebuildsw);
[170]1360
1361   GetOptions(
[172]1362      'verbose|v'          => \$verbose,
1363      'verb-description|d' => \$verb_description,
1364      'chk-hostname|h'     => \$check_hostname,
[173]1365      'chk-location|l'     => \$check_location,
[303]1366      'no-rebuildsw|R'     => \$no_rebuildsw,
[170]1367      );
1368
1369   my @network = @ARGV;
[370]1370   @network = get_list_network() if not @network;
[2]1371
[34]1372   test_switchdb_environnement();
1373
1374   my $computerdb = {};
[370]1375   $computerdb = computerdb_load() if -e "$KLASK_DB_FILE";
[2]1376   my $timestamp = time;
[22]1377
[2]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;
[63]1382   my $size_of_database   = keys %{$computerdb};
[370]1383   $size_of_database = 1 if $size_of_database == 0;
1384   my $i                 = 0;
[2]1385   my $detected_computer = 0;
[22]1386
[370]1387   init_switch_names('yes', $verb_description, $check_hostname, $check_location); # nomme les switchs
[2]1388
[296]1389   {
[404]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            }
[302]1401         }
[296]1402
[404]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:";
[22]1409         }
[404]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         }
[22]1418      }
1419
[2]1420   my %router_mac_ip = ();
1421   DETECT_ALL_ROUTER:
[370]1422   #   for my $one_router ('194.254.66.254') {
1423   for my $one_router (get_list_main_router(@network)) {
[2]1424      my %resol_arp = resolve_ip_arp_host($one_router);
[370]1425      $router_mac_ip{$resol_arp{'mac_address'}} = $resol_arp{'ipv4_address'};
[2]1426      }
1427
1428   ALL_NETWORK:
[179]1429   for my $current_net (@network) {
[2]1430
[370]1431      my @computer          = get_list_ip($current_net);
[179]1432      my $current_interface = get_current_interface($current_net);
[2]1433
[179]1434      fast_ping(@computer) if get_current_scan_mode($current_net) eq 'active';
[2]1435
1436      LOOP_ON_COMPUTER:
1437      for my $one_computer (@computer) {
1438         $i++;
[49]1439
[370]1440         my $total_percent = int(($i * 100) / $number_of_computer);
[63]1441
[2]1442         my $localtime = time - $timestamp;
[370]1443         my ($sec, $min) = localtime $localtime;
[2]1444
1445         my $time_elapse = 0;
[370]1446         $time_elapse = $localtime * (100 - $total_percent) / $total_percent if $total_percent != 0;
1447         my ($sec_elapse, $min_elapse) = localtime $time_elapse;
[2]1448
[370]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;
[96]1452         printf ' %-8s %-14s', $current_interface, $one_computer;
[2]1453
[186]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);
[63]1456
[9]1457         # do not search on router connection (why ?)
[370]1458         if (exists $router_mac_ip{$resol_arp{'mac_address'}}) {
[179]1459            $computer_not_detected{$one_computer} = $current_net;
[2]1460            next LOOP_ON_COMPUTER;
1461            }
1462
[9]1463         # do not search on switch inter-connection
[261]1464         if (exists $SWITCH_LEVEL{$resol_arp{'hostname_fq'}}) {
[179]1465            $computer_not_detected{$one_computer} = $current_net;
[2]1466            next LOOP_ON_COMPUTER;
1467            }
1468
[63]1469         my $switch_proposal = q{};
[261]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'};
[2]1472            }
1473
[3]1474         # do not have a mac address
[404]1475         if ($resol_arp{'mac_address'} eq 'unknow'
1476               or (exists $resol_arp{'timestamps'} and $resol_arp{'timestamps'} < ($timestamp - 3 * 3600))
1477               ) {
[179]1478            $computer_not_detected{$one_computer} = $current_net;
[3]1479            next LOOP_ON_COMPUTER;
1480            }
1481
[261]1482         my $vlan_name = get_current_vlan_name_for_interface($resol_arp{'interface'});
[147]1483         my $vlan_id   = get_current_vlan_id($vlan_name);
[370]1484         my %where     = find_switch_port($resol_arp{'mac_address'}, $switch_proposal, $vlan_id);
[2]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
[370]1494         #         if ($resol_arp{'mac_address'} eq 'unknow') {
1495         #            $computer_not_detected{$one_computer} = $current_interface;
1496         #            next LOOP_ON_COMPUTER;
1497         #            }
[2]1498
1499         # detected on a switch
[261]1500         if ($where{'switch_description'} ne 'unknow') {
[2]1501            $detected_computer++;
[261]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'},
[270]1507               switch_port_id     => $where{'switch_port_id'},
[261]1508               switch_port_hr     => $where{'switch_port_hr'},
[2]1509               timestamp          => $timestamp,
[179]1510               network            => $current_net,
[2]1511               };
1512            next LOOP_ON_COMPUTER;
1513            }
1514
1515         # new in the database but where it is ?
[261]1516         if (not exists $computerdb->{$resol_arp{'ipv4_address'}}) {
[2]1517            $detected_computer++;
[261]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'},
[270]1523               switch_port_id     => $where{'switch_port_id'},
[261]1524               switch_port_hr     => $where{'switch_port_hr'},
1525               timestamp          => $resol_arp{'timestamp'},
[179]1526               network            => $current_net,
[2]1527               };
1528            }
1529
1530         # mise a jour du nom de la machine si modification dans le dns
[261]1531         $computerdb->{$resol_arp{'ipv4_address'}}{'hostname_fq'} = $resol_arp{'hostname_fq'};
[63]1532
[2]1533         # mise à jour de la date de détection si détection plus récente par arpwatch
[404]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'};
[2]1536
1537         # relance un arping sur la machine si celle-ci n'a pas été détectée depuis plus d'une semaine
[404]1538         $computer_not_detected{$resol_arp{'ipv4_address'}} = $current_net
1539            if $computerdb->{$resol_arp{'ipv4_address'}}{'timestamp'} < $timestamp_last_week;
[63]1540
[2]1541         }
1542      }
1543
1544   # final end of line at the end of the loop
1545   printf "\n";
1546
[387]1547   computerdb_save($computerdb);
[2]1548
1549   for my $one_computer (keys %computer_not_detected) {
[370]1550      my $current_net       = $computer_not_detected{$one_computer};
[179]1551      my $current_interface = get_current_interface($current_net);
[404]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';
[2]1554      }
[63]1555   return;
[2]1556   }
1557
[218]1558#---------------------------------------------------------------
[2]1559sub cmd_removedb {
1560   my @computer = @_;
[34]1561
1562   test_maindb_environnement();
1563
[151]1564   my $computerdb = computerdb_load();
[2]1565
1566   LOOP_ON_COMPUTER:
1567   for my $one_computer (@computer) {
1568
[370]1569      if ($one_computer =~ m/^ $RE_IPv4_ADDRESS $/xms
1570            and exists $computerdb->{$one_computer}) {
[66]1571         delete $computerdb->{$one_computer};
1572         next;
1573         }
1574
[2]1575      my %resol_arp = resolve_ip_arp_host($one_computer);
1576
[261]1577      delete $computerdb->{$resol_arp{'ipv4_address'}} if exists $computerdb->{$resol_arp{'ipv4_address'}};
[2]1578      }
1579
[387]1580   computerdb_save($computerdb);
[63]1581   return;
[2]1582   }
1583
[218]1584#---------------------------------------------------------------
[389]1585sub cmd_insertdb {
[397]1586   local @ARGV = @_;
[383]1587
[384]1588   my ($ip, $mac, $net);
[383]1589
1590   GetOptions(
1591      'ip|i=s'      => \$ip,
[384]1592      'mac|m=s'     => \$mac,
1593      'network|n=s' => \$net,
[383]1594      );
1595
[384]1596   if (not $ip or not $mac or not $net) {
[385]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;
[384]1600      print {*STDERR} "\n";
1601      $CMD_DB{'help'}->();
1602      exit 1;
1603      }
1604
[383]1605   test_maindb_environnement();
1606
[384]1607   $mac = normalize_mac_address($mac);
[383]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') {
[389]1621      print {*STDERR} "Error: DNS name not defined for IPv4 $ip (mandatory)\n";
[383]1622      exit 1;
1623      }
1624
1625   if ($resol_arp{'ipv4_address'} ne $ip) {
[389]1626      print {*STDERR} "Error: IPv4 DNS resolution strange between $ip and $resol_arp{'ipv4_address'}\n";
[383]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) {
[389]1636         print {*STDERR} "Error: IPv4 $ip already exists in the computer database\n";
[383]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
[386]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
[383]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
[387]1659   computerdb_save($computerdb);
[383]1660   return;
1661   }
1662
1663#---------------------------------------------------------------
[74]1664sub cmd_cleandb {
[397]1665   local @ARGV = @_;
[74]1666
1667   my $days_to_clean = 15;
[188]1668   my $repairdns;
[74]1669   my $verbose;
1670   my $database_has_changed;
1671
[138]1672   GetOptions(
[370]1673      'day|d=i'      => \$days_to_clean,
1674      'verbose|v'    => \$verbose,
[190]1675      'repair-dns|r' => \$repairdns,
[74]1676      );
1677
1678   my @vlan_name = get_list_network();
1679
[188]1680   my $computerdb = computerdb_load();
[370]1681   my $timestamp  = time;
[74]1682
1683   my $timestamp_barrier = 3600 * 24 * $days_to_clean;
[106]1684   my $timestamp_3month  = 3600 * 24 * 90;
[74]1685
[104]1686   my %mactimedb = ();
[74]1687   ALL_VLAN:
[109]1688   for my $vlan (shuffle @vlan_name) {
[74]1689
[370]1690      my @ip_list = shuffle get_list_ip($vlan);
[198]1691
[74]1692      LOOP_ON_IP_ADDRESS:
1693      for my $ip (@ip_list) {
1694
[370]1695         next LOOP_ON_IP_ADDRESS
1696            if not exists $computerdb->{$ip};
[198]1697
[370]1698         #&& $computerdb->{$ip}{'timestamp'} > $timestamp_barrier;
[261]1699         my $ip_timestamp   = $computerdb->{$ip}{'timestamp'};
1700         my $ip_mac         = $computerdb->{$ip}{'mac_address'};
1701         my $ip_hostname_fq = $computerdb->{$ip}{'hostname_fq'};
[104]1702
1703         $mactimedb{$ip_mac} ||= {
1704            ip          => $ip,
1705            timestamp   => $ip_timestamp,
1706            vlan        => $vlan,
1707            hostname_fq => $ip_hostname_fq,
1708            };
[198]1709
[108]1710         if (
[261]1711            ( $mactimedb{$ip_mac}->{'timestamp'} - $ip_timestamp > $timestamp_barrier
[108]1712               or (
[261]1713                  $mactimedb{$ip_mac}->{'timestamp'} > $ip_timestamp
1714                  and $timestamp - $mactimedb{$ip_mac}->{'timestamp'} > $timestamp_3month
[108]1715                  )
1716            )
[105]1717            and (
[261]1718               not $mactimedb{$ip_mac}->{'hostname_fq'} =~ m/$RE_FLOAT_HOSTNAME/
[118]1719               or $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/
[105]1720               )) {
[74]1721            print "remove ip $ip\n" if $verbose;
1722            delete $computerdb->{$ip};
1723            $database_has_changed++;
1724            }
1725
[108]1726         elsif (
[261]1727            ( $ip_timestamp - $mactimedb{$ip_mac}->{'timestamp'} > $timestamp_barrier
[108]1728               or (
[261]1729                  $ip_timestamp > $mactimedb{$ip_mac}->{'timestamp'}
[108]1730                  and $timestamp - $ip_timestamp > $timestamp_3month
1731                  )
1732            )
[105]1733            and (
[118]1734               not $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/
[261]1735               or $mactimedb{$ip_mac}->{'hostname_fq'} =~ m/$RE_FLOAT_HOSTNAME/
[105]1736               )) {
[310]1737            print "remove ip ".$mactimedb{$ip_mac}->{'ip'}."\n" if $verbose;
1738            delete $computerdb->{$mactimedb{$ip_mac}->{'ip'}};
[74]1739            $database_has_changed++;
1740            }
1741
[370]1742         if ($ip_timestamp > $mactimedb{$ip_mac}->{'timestamp'}) {
[104]1743            $mactimedb{$ip_mac} = {
1744               ip          => $ip,
1745               timestamp   => $ip_timestamp,
1746               vlan        => $vlan,
1747               hostname_fq => $ip_hostname_fq,
1748               };
[74]1749            }
1750         }
1751      }
1752
[188]1753   if ($repairdns) { # Search and update unkown computer in reverse DNS
[189]1754      LOOP_ON_IP_ADDRESS:
1755      for my $ip (keys %{$computerdb}) {
[261]1756         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} ne 'unknow';
[188]1757
[189]1758         my $packed_ip = scalar gethostbyname($ip);
1759         next LOOP_ON_IP_ADDRESS if not defined $packed_ip;
[188]1760
1761         my $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET);
[189]1762         next LOOP_ON_IP_ADDRESS if not defined $hostname_fq;
[188]1763
[261]1764         $computerdb->{$ip}{'hostname_fq'} = $hostname_fq;
[188]1765         $database_has_changed++;
1766         }
1767      }
1768
[387]1769   computerdb_save($computerdb) if $database_has_changed;
[74]1770   return;
1771   }
1772
[218]1773#---------------------------------------------------------------
[2]1774sub cmd_exportdb {
[397]1775   local @ARGV = @_;
[45]1776
1777   my $format = 'txt';
1778
[138]1779   GetOptions(
[45]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};
[63]1789
[45]1790   $possible_format{$format}->(@ARGV);
[63]1791   return;
[45]1792   }
1793
[218]1794#---------------------------------------------------------------
[45]1795sub cmd_exportdb_txt {
[34]1796   test_maindb_environnement();
1797
[151]1798   my $computerdb = computerdb_load();
[2]1799
[320]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      );
[2]1810
1811   LOOP_ON_IP_ADDRESS:
[165]1812   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
[63]1813
[2]1814      # to be improve in the future
[404]1815      next LOOP_ON_IP_ADDRESS
1816         if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
[2]1817
[369]1818      my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $computerdb->{$ip}{'timestamp'};
[2]1819      $year += 1900;
1820      $mon++;
[63]1821      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
[2]1822
[177]1823      my $vlan = '';
[370]1824      $vlan = $computerdb->{$ip}{'network'} . '(' . get_current_vlan_id($computerdb->{$ip}{'network'}).')' if $computerdb->{$ip}{'network'};
[177]1825
[369]1826      my $arrow = '<---';
1827      $arrow = '<===' if $computerdb->{$ip}{'switch_port_hr'} =~ m/^(Trk|Br|Po)/;
[232]1828
[320]1829      $tb_computer->add(
[261]1830         $computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'},
1831         $computerdb->{$ip}{'switch_port_hr'},
[232]1832         $arrow,
[261]1833         $computerdb->{$ip}{'hostname_fq'},
[2]1834         $ip,
[261]1835         $computerdb->{$ip}{'mac_address'},
[70]1836         $date,
[320]1837         $vlan,
1838         );
[2]1839      }
[320]1840
1841   print $tb_computer->title();
1842   print $tb_computer->rule('-');
1843   print $tb_computer->body();
1844
[63]1845   return;
[2]1846   }
1847
[218]1848#---------------------------------------------------------------
[45]1849sub cmd_exportdb_html {
1850   test_maindb_environnement();
1851
[151]1852   my $computerdb = computerdb_load();
[45]1853
[369]1854   #<link rel="stylesheet" type="text/css" href="style-klask.css" />
1855   #<script src="sorttable-klask.js"></script>
[45]1856
[63]1857   print <<'END_HTML';
[349]1858<table class="sortable">
[72]1859 <caption>Klask Host Database</caption>
[45]1860 <thead>
1861  <tr>
[353]1862   <th scope="col" class="klask-header-left">Switch</th>
[45]1863   <th scope="col" class="sorttable_nosort">Port</th>
[269]1864   <th scope="col" class="sorttable_nosort">Link</th>
[353]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>
[45]1870  </tr>
1871 </thead>
1872 <tbody>
[63]1873END_HTML
[45]1874
1875   my %mac_count = ();
1876   LOOP_ON_IP_ADDRESS:
[165]1877   for my $ip (keys %{$computerdb}) {
[63]1878
[45]1879      # to be improve in the future
[261]1880      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
[63]1881
[261]1882      $mac_count{$computerdb->{$ip}{'mac_address'}}++;
[45]1883      }
1884
1885   my $typerow = 'even';
1886
1887   LOOP_ON_IP_ADDRESS:
[165]1888   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
[63]1889
[45]1890      # to be improve in the future
[261]1891      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
[45]1892
[369]1893      my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $computerdb->{$ip}{'timestamp'};
[45]1894      $year += 1900;
1895      $mon++;
[63]1896      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
[45]1897
[369]1898      #      $odd_or_even++;
1899      #      my $typerow = $odd_or_even % 2 ? 'odd' : 'even';
[63]1900      $typerow = $typerow eq 'even' ? 'odd' : 'even';
[45]1901
[265]1902      #my $arrow ='&#8592;';
1903      #   $arrow ='&#8656;' if $computerdb->{$ip}{'switch_port_hr'} =~ m/^(Trk|Br|Po)/;
[369]1904      my $arrow = '&#10229;';
1905      $arrow = '&#10232;' if $computerdb->{$ip}{'switch_port_hr'} =~ m/^(Trk|Br|Po)/;
[163]1906
[261]1907      my $switch_hostname = $computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'} || 'unkown';
[45]1908      chomp $switch_hostname;
[348]1909      my $switch_hostname_sort = format_switchport4sort($switch_hostname, $computerdb->{$ip}{'switch_port_hr'});
[45]1910
[63]1911      my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ip;
[45]1912
[261]1913      my $mac_sort = sprintf '%04i-%s', 9999 - $mac_count{$computerdb->{$ip}{'mac_address'}}, $computerdb->{$ip}{'mac_address'};
[45]1914
[261]1915      $computerdb->{$ip}{'hostname_fq'} = 'unknow' if $computerdb->{$ip}{'hostname_fq'} =~ m/^ \d+ \. \d+ \. \d+ \. \d+ $/xms;
[369]1916      my ($host_short) = split m/ \. /xms, $computerdb->{$ip}{'hostname_fq'};
[45]1917
[178]1918      my $vlan = '';
[404]1919      $vlan = $computerdb->{$ip}{'network'} . ' (' . get_current_vlan_id($computerdb->{$ip}{'network'}) . ')'
1920         if $computerdb->{$ip}{'network'};
[71]1921
[261]1922      my $parent_port_hr = format_aggregator4html($computerdb->{$ip}{'switch_port_hr'});
[253]1923
[317]1924      fqdn_html_breakable($switch_hostname);
1925      fqdn_html_breakable(my $hostname_fq_html = $computerdb->{$ip}{'hostname_fq'});
1926
[63]1927      print <<"END_HTML";
[45]1928  <tr class="$typerow">
1929   <td sorttable_customkey="$switch_hostname_sort">$switch_hostname</td>
[253]1930   <td class="bklask-port">$parent_port_hr</td>
[314]1931   <td class="bklask-arrow">$arrow</td>
[317]1932   <td sorttable_customkey="$host_short">$hostname_fq_html</td>
[45]1933   <td sorttable_customkey="$ip_sort">$ip</td>
[261]1934   <td sorttable_customkey="$mac_sort">$computerdb->{$ip}{'mac_address'}</td>
[71]1935   <td>$vlan</td>
[45]1936   <td>$date</td>
1937  </tr>
[63]1938END_HTML
[45]1939      }
1940
1941   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
1942
[261]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'}};
[45]1947
[266]1948   # Output switch connection
1949   LOOP_ON_OUTPUT_SWITCH:
[45]1950   for my $sw (sort keys %db_switch_output_port) {
1951
[348]1952      my $switch_hostname_sort = format_switchport4sort($sw, $db_switch_output_port{$sw});
[45]1953
[63]1954      $typerow = $typerow eq 'even' ? 'odd' : 'even';
[45]1955
[264]1956      #my $arrow ='&#8702;';
1957      #   $arrow ='&#8680;' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
[369]1958      my $arrow = '&#10236;';
1959      $arrow = '&#10238;' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
[163]1960
[45]1961      if (exists $db_switch_parent{$sw}) {
[266]1962         # Link to uplink switch
1963         next LOOP_ON_OUTPUT_SWITCH;
[290]1964
[266]1965         # Do not print anymore
[261]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'};
[45]1969
[369]1970         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $timestamp;
[163]1971         $year += 1900;
1972         $mon++;
1973         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
[45]1974
[205]1975         my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ [\.\*] /xms, $ipv4_address; # \* for fake-ip
[45]1976
[163]1977         my $mac_sort = sprintf '%04i-%s', 9999, $mac_address;
[45]1978
[348]1979         my $host_short = format_switchport4sort(split(m/ \. /xms, $db_switch_parent{$sw}->{'switch'}, 1), $db_switch_parent{$sw}->{'port_hr'});
[45]1980
[263]1981         my $vlan = $db_switch{$db_switch_parent{$sw}->{'switch'}}->{'network'};
[370]1982         $vlan .= ' (' . get_current_vlan_id($db_switch{$db_switch_parent{$sw}->{'switch'}}->{'network'}) . ')' if $db_switch{$db_switch_parent{$sw}->{'switch'}}->{'network'};
[263]1983
[253]1984         my $parent_port_hr = format_aggregator4html($db_switch_output_port{$sw});
[261]1985         my $child_port_hr  = format_aggregator4html($db_switch_parent{$sw}->{'port_hr'});
[253]1986
[317]1987         fqdn_html_breakable($sw);
1988         fqdn_html_breakable(my $sw_child_html = $db_switch_parent{$sw}->{'switch'});
1989
[163]1990         print <<"END_HTML";
[45]1991  <tr class="$typerow">
1992   <td sorttable_customkey="$switch_hostname_sort">$sw</td>
[253]1993   <td class="bklask-port">$parent_port_hr</td>
[314]1994   <td class="bklask-arrow">$arrow $child_port_hr</td>
[317]1995   <td sorttable_customkey="$host_short">$sw_child_html</td>
[45]1996   <td sorttable_customkey="$ip_sort">$ipv4_address</td>
1997   <td sorttable_customkey="$mac_sort">$mac_address</td>
[263]1998   <td>$vlan</td>
[45]1999   <td>$date</td>
2000  </tr>
[63]2001END_HTML
[45]2002         }
2003      else {
[266]2004         # Router
[253]2005         my $parent_port_hr = format_aggregator4html($db_switch_output_port{$sw});
2006
[348]2007         my $host_short = format_switchport4sort($sw, $db_switch_output_port{$sw});
[266]2008
[369]2009         my $mac_address  = $db_switch{$sw}->{'mac_address'};
[266]2010         my $ipv4_address = $db_switch{$sw}->{'ipv4_address'};
[369]2011         my $timestamp    = $db_switch{$sw}->{'timestamp'};
[266]2012
[369]2013         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $timestamp;
[266]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'};
[369]2023         $vlan .= ' (' . get_current_vlan_id($db_switch{$sw}->{'network'}) . ')' if $db_switch{$sw}->{'network'};
[266]2024
[369]2025         my $arrow = '&#10235;';
2026         $arrow = '&#10237;' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
[266]2027
[317]2028         fqdn_html_breakable($sw);
2029
[63]2030         print <<"END_HTML";
[45]2031  <tr class="$typerow">
[266]2032   <td sorttable_customkey="router">router</td>
2033   <td class="bklask-port"></td>
[314]2034   <td class="bklask-arrow">$arrow $parent_port_hr</td>
[266]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;
[45]2043         }
2044      }
2045
[266]2046   # Child switch connection : parent <- child
[45]2047   for my $swport (sort keys %db_switch_connected_on_port) {
[248]2048      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
[45]2049      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
2050
[348]2051         my $switch_hostname_sort = format_switchport4sort($sw_connect, $port_connect);
[45]2052
[369]2053         my $mac_address  = $db_switch{$sw}->{'mac_address'};
[261]2054         my $ipv4_address = $db_switch{$sw}->{'ipv4_address'};
[369]2055         my $timestamp    = $db_switch{$sw}->{'timestamp'};
[45]2056
[369]2057         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $timestamp;
[163]2058         $year += 1900;
2059         $mon++;
[369]2060         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
[45]2061
[205]2062         my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ [\.\*] /xms, $ipv4_address; # \* for fake-ip
[45]2063
[163]2064         my $mac_sort = sprintf '%04i-%s', 9999, $mac_address;
[45]2065
[163]2066         $typerow = $typerow eq 'even' ? 'odd' : 'even';
[45]2067
[264]2068         #my $arrow ='&#8701;';
2069         #   $arrow ='&#8678;' if $port_connect =~ m/^(Trk|Br|Po)/;
[369]2070         my $arrow = '&#10235;';
2071         $arrow = '&#10237;' if $port_connect =~ m/^(Trk|Br|Po)/;
[163]2072
[263]2073         my $vlan = $db_switch{$sw}->{'network'};
[369]2074         $vlan .= ' (' . get_current_vlan_id($db_switch{$sw}->{'network'}) . ')' if $db_switch{$sw}->{'network'};
[263]2075
[45]2076         if (exists $db_switch_output_port{$sw}) {
2077
[369]2078            my $host_short = format_switchport4sort(split(m/\./xms, $sw, 1), $db_switch_output_port{$sw});
[45]2079
[253]2080            my $parent_port_hr = format_aggregator4html($port_connect);
2081            my $child_port_hr  = format_aggregator4html($db_switch_output_port{$sw});
2082
[317]2083            fqdn_html_breakable($sw);
2084            fqdn_html_breakable($sw_connect);
2085
[63]2086            print <<"END_HTML";
[45]2087  <tr class="$typerow">
2088   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
[253]2089   <td class="bklask-port">$parent_port_hr</td>
[314]2090   <td class="bklask-arrow">$arrow $child_port_hr</td>
[162]2091   <td sorttable_customkey="$host_short">$sw</td>
[45]2092   <td sorttable_customkey="$ip_sort">$ipv4_address</td>
2093   <td sorttable_customkey="$mac_sort">$mac_address</td>
[263]2094   <td>$vlan</td>
[45]2095   <td>$date</td>
2096  </tr>
[63]2097END_HTML
[45]2098            }
2099         else {
[253]2100            my $parent_port_hr = format_aggregator4html($port_connect);
2101
[317]2102            fqdn_html_breakable($sw);
2103            fqdn_html_breakable($sw_connect);
2104
[63]2105            print <<"END_HTML";
[45]2106  <tr class="$typerow">
2107   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
[253]2108   <td class="bklask-port">$parent_port_hr</td>
[314]2109   <td class="bklask-arrow">$arrow</td>
[162]2110   <td sorttable_customkey="$sw">$sw</td>
[45]2111   <td sorttable_customkey="">$ipv4_address</td>
2112   <td sorttable_customkey="">$mac_address</td>
[263]2113   <td>$vlan</td>
[45]2114   <td>$date</td>
2115  </tr>
[63]2116END_HTML
[45]2117            }
2118         }
2119      }
2120
[63]2121   print <<'END_HTML';
[45]2122 </tbody>
[349]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>
[45]2135</table>
[63]2136END_HTML
2137   return;
[45]2138   }
2139
[218]2140#---------------------------------------------------------------
[133]2141sub cmd_bad_vlan_id {
[397]2142   local @ARGV = @_;
[224]2143
2144   my $days_before_alert = $DEFAULT{'days-before-alert'} || 15;
[326]2145   my $format = 'txt';
[224]2146   my $verbose;
2147
2148   GetOptions(
[369]2149      'day|d=i'    => \$days_before_alert,
2150      'format|f=s' => \$format,
[224]2151      );
2152
[326]2153   my %possible_format = (
2154      txt  => \&cmd_bad_vlan_id_txt,
2155      html => \&cmd_bad_vlan_id_html,
[369]2156      none => sub { },
[326]2157      );
2158   $format = 'txt' if not defined $possible_format{$format};
2159
[114]2160   test_maindb_environnement();
2161
[151]2162   my $computerdb = computerdb_load();
[114]2163
[132]2164   # create a database with the most recent computer by switch port
[211]2165   my %switchportdb = ();
[114]2166   LOOP_ON_IP_ADDRESS:
[165]2167   for my $ip (keys %{$computerdb}) {
[388]2168      # to be really improve in the future
[261]2169      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
[388]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';
[114]2172
[261]2173      my $ip_timestamp   = $computerdb->{$ip}{'timestamp'};
2174      my $ip_mac         = $computerdb->{$ip}{'mac_address'};
2175      my $ip_hostname_fq = $computerdb->{$ip}{'hostname_fq'};
[114]2176
[115]2177      my $swpt = sprintf "%-28s  %2s",
[261]2178         $computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'},
2179         $computerdb->{$ip}{'switch_port_hr'};
[211]2180      $switchportdb{$swpt} ||= {
[114]2181         ip          => $ip,
2182         timestamp   => $ip_timestamp,
[262]2183         vlan        => $computerdb->{$ip}{'network'},
[114]2184         hostname_fq => $ip_hostname_fq,
2185         mac_address => $ip_mac,
2186         };
[117]2187
[132]2188      # if float computer, set date 15 day before warning...
2189      my $ip_timestamp_mod = $ip_timestamp;
[261]2190      my $ip_timestamp_ref = $switchportdb{$swpt}->{'timestamp'};
[224]2191      $ip_timestamp_mod -= $days_before_alert * 24 * 3600 if $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;
[261]2192      $ip_timestamp_ref -= $days_before_alert * 24 * 3600 if $switchportdb{$swpt}->{'hostname_fq'} =~ m/$RE_FLOAT_HOSTNAME/;
[198]2193
[132]2194      if ($ip_timestamp_mod > $ip_timestamp_ref) {
[211]2195         $switchportdb{$swpt} = {
[114]2196            ip          => $ip,
2197            timestamp   => $ip_timestamp,
[262]2198            vlan        => $computerdb->{$ip}{'network'},
[114]2199            hostname_fq => $ip_hostname_fq,
2200            mac_address => $ip_mac,
2201            };
2202         }
2203      }
2204
[326]2205   my @result = ();
[321]2206
[133]2207   LOOP_ON_RECENT_COMPUTER:
[211]2208   for my $swpt (keys %switchportdb) {
[133]2209      next LOOP_ON_RECENT_COMPUTER if $swpt =~ m/^\s*0$/;
[261]2210      next LOOP_ON_RECENT_COMPUTER if $switchportdb{$swpt}->{'hostname_fq'} !~ m/$RE_FLOAT_HOSTNAME/;
[117]2211
[369]2212      my $src_ip        = $switchportdb{$swpt}->{'ip'};
[114]2213      my $src_timestamp = 0;
[133]2214      LOOP_ON_IP_ADDRESS:
[165]2215      for my $ip (keys %{$computerdb}) {
[369]2216         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'mac_address'} ne $switchportdb{$swpt}->{'mac_address'};
[261]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;
[198]2219
[369]2220         $src_ip        = $ip;
[261]2221         $src_timestamp = $computerdb->{$ip}{'timestamp'};
[114]2222         }
[132]2223
2224      # keep only if float computer is the most recent
[133]2225      next LOOP_ON_RECENT_COMPUTER if $src_timestamp == 0;
[261]2226      next LOOP_ON_RECENT_COMPUTER if $switchportdb{$swpt}->{'timestamp'} < $src_timestamp;
[398]2227      next LOOP_ON_RECENT_COMPUTER if $computerdb->{$src_ip}{'hostname_fq'} eq 'unknow';
[132]2228
[369]2229      my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $switchportdb{$swpt}->{'timestamp'};
[114]2230      $year += 1900;
2231      $mon++;
[132]2232      my $date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;
[114]2233
[369]2234      ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $computerdb->{$src_ip}{'timestamp'};
[114]2235      $year += 1900;
2236      $mon++;
[132]2237      my $src_date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;
[114]2238
[262]2239      my $vlan_id = get_current_vlan_id($computerdb->{$src_ip}{'network'});
[321]2240      my ($switch_hostname, $port_hr) = split /\s+/, $swpt, 2;
[369]2241
[326]2242      push @result, {
2243         switch      => $switch_hostname,
2244         port_hr     => $port_hr,
2245         vlan_bad    => $switchportdb{$swpt}->{'vlan'},
[369]2246         vlan_good   => $computerdb->{$src_ip}{'network'},
[326]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 {
[327]2260   my @result = @_;
[326]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) {
[321]2283      $tb_bad->add(
[326]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'},
[321]2293         );
[114]2294      }
[326]2295
[322]2296   print $tb_bad->title();
2297   print $tb_bad->rule('-');
[321]2298   print $tb_bad->body();
[114]2299   }
2300
[218]2301#---------------------------------------------------------------
[326]2302sub cmd_bad_vlan_id_html {
[327]2303   my @result = @_;
[326]2304
2305   print <<'END_HTML';
[349]2306<table class="sortable">
2307 <caption>Klask VLAN ID Mismatch Database</caption>
[326]2308 <thead>
2309  <tr>
[353]2310   <th scope="col" class="klask-header-left">Switch</th>
[326]2311   <th scope="col" class="sorttable_nosort">Port</th>
[353]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>
[326]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
[348]2329      my $switch_hostname_sort = format_switchport4sort($item->{'switch'}, $item->{'port_hr'});
[369]2330      my ($host_short) = split m/ \. /xms, $item->{'hostname_fq'};
[326]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>
[330]2339   <td class="bklask-port">$item->{'port_hr'}</td>
[326]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>
[349]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>
[326]2363</table>
2364END_HTML
2365   }
2366
2367#---------------------------------------------------------------
[239]2368sub cmd_poe_enable {
[397]2369   local @ARGV = @_;
[239]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
[277]2388      my $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number
[239]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(
[277]2395         -varbindlist => [$oid_search, INTEGER, 8], # Only NEXANS
[239]2396         );
[369]2397      print $session->error() . "\n" if $session->error_status();
[239]2398
2399      $session->close;
2400      }
2401   cmd_poe_status($switch_name, $switch_port);
2402   return;
2403   }
2404
2405#---------------------------------------------------------------
2406sub cmd_poe_disable {
[397]2407   local @ARGV = @_;
[239]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
[277]2426      my $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number
[239]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(
[277]2433         -varbindlist => [$oid_search, INTEGER, 2], # Only NEXANS
[239]2434         );
[369]2435      print $session->error() . "\n" if $session->error_status();
[239]2436
2437      $session->close;
2438      }
2439   cmd_poe_status($switch_name, $switch_port);
2440   return;
2441   }
2442
2443#---------------------------------------------------------------
2444sub cmd_poe_status {
[397]2445   local @ARGV = @_;
[239]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
[277]2464      my $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number
[239]2465
2466      my $sw = $SWITCH_DB{$sw_name};
[369]2467      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
[239]2468      print "$error \n" if $error;
2469
2470      my $result = $session->get_request(
[277]2471         -varbindlist => [$oid_search],
[239]2472         );
2473
[277]2474      if (defined $result and $result->{$oid_search} ne 'noSuchInstance') {
2475         my $poe_status = $result->{$oid_search} || 'empty';
[276]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;
[239]2479         }
2480      else {
[276]2481         print "Klask do not find PoE status on switch $sw_name on port $switch_port\n";
[239]2482         }
2483
2484      $session->close;
2485      }
2486   return;
2487   }
2488
2489#---------------------------------------------------------------
[305]2490sub cmd_host_setlocation {
[397]2491   local @ARGV = @_;
[305]2492
2493   my ($verbose, $force);
2494   GetOptions(
2495      'verbose|v' => \$verbose,
2496      'force|f'   => \$force,
2497      );
2498
[369]2499   my $switch_name     = shift @ARGV || q{};
[305]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#---------------------------------------------------------------
[220]2528# not finish - do not use
[238]2529sub cmd_port_setvlan {
[129]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
[369]2537   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST) if $switch_name eq q{*};
[129]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
[277]2544      my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
[369]2545      my $oid_search_port2 = $OID_NUMBER{'searchPort2'} . '.' . 0 . mac_address_hex2dec($mac_address);
[277]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";
[129]2548
[209]2549      my $sw = $SWITCH_DB{$sw_name};
[369]2550      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
[129]2551      print "$error \n" if $error;
2552
2553      my $result = $session->get_request(
[277]2554         -varbindlist => [$oid_search_port1]
[129]2555         );
2556      if (not defined $result) {
2557         $result = $session->get_request(
[277]2558            -varbindlist => [$oid_search_port2]
[129]2559            );
[277]2560         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
[129]2561         }
2562
[277]2563      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
2564         my $swport = $result->{$oid_search_port1};
[129]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
[218]2576#---------------------------------------------------------------
[238]2577sub cmd_port_getvlan {
[397]2578   local @ARGV = @_;
[141]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{}) {
[238]2589      die "Usage: klask port-getvlan SWITCH_NAME PORT\n";
[141]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
[277]2597      my $oid_search = $OID_NUMBER{'vlanPortDefault'} . ".$switch_port";
[141]2598
[209]2599      my $sw = $SWITCH_DB{$sw_name};
[369]2600      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
[141]2601      print "$error \n" if $error;
2602
2603      my $result = $session->get_request(
[277]2604         -varbindlist => [$oid_search],
[141]2605         );
2606
[277]2607      if (defined $result and $result->{$oid_search} ne 'noSuchInstance') {
2608         my $vlan_id = $result->{$oid_search} || 'empty';
[141]2609         print "Klask VLAN Id $vlan_id on switch $sw_name on port $switch_port\n";
2610         }
2611      else {
[142]2612         print "Klask do not find VLAN Id on switch $sw_name on port $switch_port\n";
[141]2613         }
2614
2615      $session->close;
2616      }
2617   return;
[129]2618   }
2619
[218]2620#---------------------------------------------------------------
[238]2621sub cmd_vlan_setname {
[129]2622   }
2623
[218]2624#---------------------------------------------------------------
[239]2625# snmpset -v 1 -c public sw1-batG0-legi.hmg.priv "$OID_NUMBER{'HPicfReset'}.0" i 2;
[141]2626sub cmd_rebootsw {
[397]2627   local @ARGV = @_;
[141]2628
2629   my $verbose;
2630   GetOptions(
2631      'verbose|v' => \$verbose,
2632      );
2633
2634   my $switch_name = shift @ARGV || q{};
2635
[142]2636   if ($switch_name eq q{}) {
[141]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};
[220]2646      my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
[141]2647      print "$error \n" if $error;
2648
2649      my $result = $session->set_request(
[239]2650         -varbindlist => ["$OID_NUMBER{'HPicfReset'}.0", INTEGER, 2],
[141]2651         );
2652
2653      $session->close;
2654      }
2655   return;
2656   }
2657
[218]2658#---------------------------------------------------------------
[238]2659sub cmd_vlan_getname {
[129]2660   my $switch_name = shift || q{};
2661   my $vlan_id     = shift || q{};
2662
2663   if ($switch_name eq q{} or $vlan_id eq q{}) {
[238]2664      die "Usage: klask vlan-getname SWITCH_NAME VLAN_ID\n";
[129]2665      }
2666
[369]2667   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST) if $switch_name eq q{*};
[129]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
[277]2674      my $oid_search_vlan_name = $OID_NUMBER{'vlanName'} . ".$vlan_id";
[129]2675
[209]2676      my $sw = $SWITCH_DB{$sw_name};
[369]2677      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
[129]2678      print "$error \n" if $error;
2679
2680      my $result = $session->get_request(
[277]2681         -varbindlist => [$oid_search_vlan_name]
[129]2682         );
2683
[277]2684      if (defined $result and $result->{$oid_search_vlan_name} ne 'noSuchInstance') {
2685         my $vlan_name = $result->{$oid_search_vlan_name} || 'empty';
[130]2686         print "Klask find VLAN $vlan_id on switch $sw_name with name $vlan_name\n";
[129]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
[218]2697#---------------------------------------------------------------
[238]2698sub cmd_vlan_list {
[237]2699   my $switch_name = shift || q{};
2700
2701   if ($switch_name eq q{}) {
[238]2702      die "Usage: klask vlan-list SWITCH_NAME\n";
[237]2703      }
2704
[369]2705   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST) if $switch_name eq q{*};
[237]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};
[369]2713      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
[237]2714      print "$error \n" if $error;
2715
2716      my %vlandb = snmp_get_vlan_list($session);
2717      $session->close;
[290]2718
[237]2719      print "VLAN_ID - VLAN_NAME # $sw_name\n";
[275]2720      for my $vlan_id (keys %vlandb) {
2721         printf "%7i - %s\n", $vlan_id, $vlandb{$vlan_id};
[237]2722         }
2723      }
2724   return;
2725   }
2726
2727#---------------------------------------------------------------
[111]2728sub cmd_ip_location {
[151]2729   my $computerdb = computerdb_load();
[2]2730
2731   LOOP_ON_IP_ADDRESS:
[165]2732   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {
[2]2733
[261]2734      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} eq ($computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'}); # switch on himself !
[2]2735
[261]2736      my $sw_hostname = $computerdb->{$ip}{'switch_hostname'} || q{};
[133]2737      next LOOP_ON_IP_ADDRESS if $sw_hostname eq 'unknow';
[63]2738
2739      my $sw_location = q{};
[133]2740      LOOP_ON_ALL_SWITCH:
[196]2741      for my $sw (@SWITCH_LIST) {
[261]2742         next LOOP_ON_ALL_SWITCH if $sw_hostname ne $sw->{'hostname'};
2743         $sw_location = $sw->{'location'};
[2]2744         last;
2745         }
2746
[63]2747      printf "%s: \"%s\"\n", $ip, $sw_location if not $sw_location eq q{};
[2]2748      }
[63]2749   return;
[2]2750   }
2751
[218]2752#---------------------------------------------------------------
[69]2753sub cmd_ip_free {
[397]2754   local @ARGV = @_;
[69]2755
[224]2756   my $days_to_death = $DEFAULT{'days-to-death'} || 365 * 2;
[69]2757   my $format = 'txt';
[97]2758   my $verbose;
[69]2759
[138]2760   GetOptions(
[369]2761      'day|d=i'    => \$days_to_death,
2762      'format|f=s' => \$format,
2763      'verbose|v'  => \$verbose,
[69]2764      );
2765
[72]2766   my %possible_format = (
2767      txt  => \&cmd_ip_free_txt,
2768      html => \&cmd_ip_free_html,
[369]2769      none => sub { },
[72]2770      );
2771   $format = 'txt' if not defined $possible_format{$format};
2772
[110]2773   my @vlan_name = @ARGV;
[69]2774   @vlan_name = get_list_network() if not @vlan_name;
2775
2776   my $computerdb = {};
[369]2777   $computerdb = computerdb_load() if -e "$KLASK_DB_FILE";
[69]2778   my $timestamp = time;
2779
[224]2780   my $timestamp_barrier = $timestamp - (3600 * 24 * $days_to_death);
[69]2781
[72]2782   my %result_ip = ();
[69]2783
2784   ALL_NETWORK:
2785   for my $vlan (@vlan_name) {
2786
2787      my @ip_list = get_list_ip($vlan);
[97]2788
[69]2789      LOOP_ON_IP_ADDRESS:
2790      for my $ip (@ip_list) {
2791
[135]2792         if (exists $computerdb->{$ip}) {
[261]2793            next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'timestamp'} > $timestamp_barrier;
[198]2794
[261]2795            my $mac_address = $computerdb->{$ip}{'mac_address'};
[135]2796            LOOP_ON_DATABASE:
[165]2797            for my $ip_db (keys %{$computerdb}) {
[404]2798               next LOOP_ON_DATABASE   if $computerdb->{$ip_db}{'mac_address'} ne $mac_address;
[261]2799               next LOOP_ON_IP_ADDRESS if $computerdb->{$ip_db}{'timestamp'} > $timestamp_barrier;
[135]2800               }
2801            }
[69]2802
2803         my $ip_date_last_detection = '';
2804         if (exists $computerdb->{$ip}) {
[369]2805            my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $computerdb->{$ip}{'timestamp'};
[69]2806            $year += 1900;
2807            $mon++;
2808            $ip_date_last_detection = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
2809            }
2810
[369]2811         my $packed_ip   = scalar gethostbyname($ip);
[69]2812         my $hostname_fq = 'unknown';
[404]2813            $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET) || 'unknown'
2814               if defined $packed_ip and get_current_scan_mode($vlan) eq 'active';
[135]2815
2816         next LOOP_ON_IP_ADDRESS if $hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;
2817
2818         $result_ip{$ip} ||= {};
[261]2819         $result_ip{$ip}->{'date_last_detection'} = $ip_date_last_detection;
[369]2820         $result_ip{$ip}->{'hostname_fq'}         = $hostname_fq;
2821         $result_ip{$ip}->{'vlan'}                = $vlan;
[97]2822
2823         printf "VERBOSE_1: %-15s %-12s %s\n", $ip, $vlan, $hostname_fq if $verbose;
[69]2824         }
2825      }
2826
[72]2827   $possible_format{$format}->(%result_ip);
2828   }
2829
[218]2830#---------------------------------------------------------------
[72]2831sub cmd_ip_free_txt {
2832   my %result_ip = @_;
[198]2833
[324]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";
[72]2843   LOOP_ON_IP_ADDRESS:
[165]2844   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {
[369]2845      my $vlan_nameid = $result_ip{$ip}->{'vlan'} . '(' . get_current_vlan_id($result_ip{$ip}->{'vlan'}) . ')';
[324]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'},
[325]2852         );
[69]2853      }
[324]2854   print $tb_computer->title();
2855   print $tb_computer->rule('-');
2856   print $tb_computer->body();
[69]2857   }
2858
[218]2859#---------------------------------------------------------------
[72]2860sub cmd_ip_free_html {
2861   my %result_ip = @_;
2862
2863   print <<'END_HTML';
[349]2864<table class="sortable">
[389]2865 <caption>Klask Free IPv4 Database</caption>
[72]2866 <thead>
2867  <tr>
[353]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>
[72]2872  </tr>
2873 </thead>
2874 <tbody>
2875END_HTML
2876
2877   my $typerow = 'even';
2878
2879   LOOP_ON_IP_ADDRESS:
[165]2880   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {
[72]2881
2882      $typerow = $typerow eq 'even' ? 'odd' : 'even';
2883
2884      my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ip;
[369]2885      my ($host_short) = split m/ \. /xms, $result_ip{$ip}->{'hostname_fq'};
[72]2886
[369]2887      my $vlan_nameid = $result_ip{$ip}->{'vlan'} . ' (' . get_current_vlan_id($result_ip{$ip}->{'vlan'}) . ')';
[180]2888
[324]2889      fqdn_html_breakable(my $hostname_fq_html = $result_ip{$ip}->{'hostname_fq'});
2890
[72]2891      print <<"END_HTML";
2892  <tr class="$typerow">
2893   <td sorttable_customkey="$ip_sort">$ip</td>
[324]2894   <td sorttable_customkey="$host_short">$hostname_fq_html</td>
[180]2895   <td>$vlan_nameid</td>
[261]2896   <td>$result_ip{$ip}->{'date_last_detection'}</td>
[72]2897  </tr>
2898END_HTML
2899      }
2900   print <<'END_HTML';
2901 </tbody>
[349]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>
[72]2910</table>
2911END_HTML
2912   }
2913
[218]2914#---------------------------------------------------------------
[2]2915sub cmd_enable {
[397]2916   local @ARGV = @_;
[63]2917
[210]2918   my $verbose;
2919
2920   GetOptions(
2921      'verbose|v' => \$verbose,
2922      );
2923
2924   my $switch_name = shift @ARGV || q{};
[276]2925   my $port_hr     = shift @ARGV || q{};
[210]2926
[276]2927   if ($switch_name eq q{} or $port_hr eq q{}) {
[210]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};
[220]2936   my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
[210]2937   print "$error \n" if $error;
2938
[216]2939   # Retrieve numeric port value
[276]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+$/;
[216]2942
[369]2943   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} . '.' . $port_id;
[277]2944   print "Info: switch $switch_name port $port_hr SNMP OID $oid_search_portstatus\n" if $verbose;
[216]2945
[210]2946   my $result = $session->set_request(
[277]2947      -varbindlist => [$oid_search_portstatus, INTEGER, 1],
[210]2948      );
[216]2949   print $session->error()."\n" if $session->error_status();
[210]2950
2951   $session->close;
2952
[63]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)
[210]2955   #system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 1";
[63]2956   return;
[2]2957   }
2958
[218]2959#---------------------------------------------------------------
[2]2960sub cmd_disable {
[397]2961   local @ARGV = @_;
[63]2962
[210]2963   my $verbose;
2964
2965   GetOptions(
2966      'verbose|v' => \$verbose,
2967      );
2968
2969   my $switch_name = shift @ARGV || q{};
[276]2970   my $port_hr     = shift @ARGV || q{};
[210]2971
[276]2972   if ($switch_name eq q{} or $port_hr eq q{}) {
[210]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};
[220]2981   my ($session, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
[210]2982   print "$error \n" if $error;
2983
[216]2984   # Retrieve numeric port value
[276]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+$/;
[216]2987
[369]2988   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} . '.' . $port_id;
[277]2989   print "Info: switch $switch_name port $port_hr SNMP OID $oid_search_portstatus\n" if $verbose;
[216]2990
[210]2991   my $result = $session->set_request(
[277]2992      -varbindlist => [$oid_search_portstatus, INTEGER, 2],
[210]2993      );
[213]2994   print $session->error()."\n" if $session->error_status();
[210]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";
[63]2999   return;
[2]3000   }
3001
[218]3002#---------------------------------------------------------------
[2]3003sub cmd_status {
[397]3004   local @ARGV = @_;
[63]3005
[206]3006   my $verbose;
3007
3008   GetOptions(
3009      'verbose|v' => \$verbose,
3010      );
3011
3012   my $switch_name = shift @ARGV || q{};
[276]3013   my $port_hr     = shift @ARGV || q{};
[206]3014
[276]3015   if ($switch_name eq q{} or $port_hr eq q{}) {
[207]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
[206]3023   my $sw = $SWITCH_DB{$switch_name};
[369]3024   my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
[206]3025   print "$error \n" if $error;
3026
[213]3027   # Retrieve numeric port value
[276]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+$/;
[213]3030
[369]3031   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} . '.' . $port_id;
[277]3032   print "Info: switch $switch_name port $port_hr ($port_id) SNMP OID $oid_search_portstatus\n" if $verbose;
[213]3033
[206]3034   my $result = $session->get_request(
[277]3035      -varbindlist => [$oid_search_portstatus]
[206]3036      );
[216]3037   print $session->error()."\n" if $session->error_status();
[206]3038   if (defined $result) {
[277]3039      print "$PORT_UPDOWN{$result->{$oid_search_portstatus}}\n";
[206]3040      }
3041
[216]3042   $session->close;
3043
[206]3044   #system "snmpget -v 1 -c public $switch_name 1.3.6.1.2.1.2.2.1.7.$port";
[63]3045   return;
[2]3046   }
3047
[218]3048#---------------------------------------------------------------
[35]3049sub cmd_search_mac_on_switch {
[397]3050   local @ARGV = @_;
[63]3051
[138]3052   my $verbose;
[144]3053   my $vlan_id = 0;
[138]3054
3055   GetOptions(
3056      'verbose|v' => \$verbose,
[144]3057      'vlan|l=i'  => \$vlan_id,
[138]3058      );
3059
3060   my $switch_name = shift @ARGV || q{};
3061   my $mac_address = shift @ARGV || q{};
3062
[63]3063   if ($switch_name eq q{} or $mac_address eq q{}) {
[39]3064      die "Usage: klask search-mac-on-switch SWITCH_NAME MAC_ADDRESS\n";
[38]3065      }
[39]3066
[138]3067   $mac_address = normalize_mac_address($mac_address);
[369]3068   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST) if $switch_name eq q{*} or $switch_name eq q{all};
[39]3069
[112]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         }
[39]3074
[277]3075      my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
[369]3076      my $oid_search_port2 = $OID_NUMBER{'searchPort2'} . '.' . $vlan_id . mac_address_hex2dec($mac_address);
[277]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;
[35]3079
[209]3080      my $sw = $SWITCH_DB{$sw_name};
[369]3081      my ($session, $error) = Net::SNMP->session(%{$sw->{'snmp_param_session'}});
[112]3082      print "$error \n" if $error;
[63]3083
[112]3084      my $result = $session->get_request(
[277]3085         -varbindlist => [$oid_search_port1]
[112]3086         );
[124]3087      if (not defined $result) {
3088         $result = $session->get_request(
[277]3089            -varbindlist => [$oid_search_port2]
[124]3090            );
[277]3091         $result->{$oid_search_port1} = $result->{$oid_search_port2} if defined $result;
[124]3092         }
[112]3093
[277]3094      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
3095         my $swport_id = $result->{$oid_search_port1};
[271]3096         my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);
[181]3097         print "Klask find MAC $mac_address on switch $sw_name port $swport_hr\n";
[123]3098         }
3099      else {
[138]3100         print "Klask do not find MAC $mac_address on switch $sw_name\n" if $verbose;
[112]3101         }
3102
[35]3103      $session->close;
3104      }
[63]3105   return;
[35]3106   }
3107
[218]3108#---------------------------------------------------------------
[298]3109sub cmd_updatesw {
[397]3110   local @ARGV = @_;
[298]3111
3112   my $verbose;
3113
3114   GetOptions(
3115      'verbose|v' => \$verbose,
3116      );
3117   
3118   update_switchdb(verbose => $verbose);
3119   }
3120
3121#---------------------------------------------------------------
[4]3122sub cmd_exportsw {
[397]3123   local @ARGV = @_;
[2]3124
[34]3125   test_switchdb_environnement();
3126
[369]3127   my $format       = 'txt';
[289]3128   my $graph_modulo = 0;
3129   my $graph_shift  = 1;
[376]3130   my $way          = 'all';
[379]3131   my $no_header;
[4]3132
[138]3133   GetOptions(
[379]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,
[4]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};
[63]3147
[379]3148   $possible_format{$format}->(modulo => $graph_modulo, shift => $graph_shift, way => $way, 'no-header' => $no_header, @ARGV);
[63]3149   return;
[4]3150   }
3151
[218]3152#---------------------------------------------------------------
[4]3153sub cmd_exportsw_txt {
[313]3154   my %args = (
[379]3155      'way'       => 'all',
3156      'no-header' => undef,
[313]3157      @_);
3158
[44]3159   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
[404]3160   my $interway          = 0;
[4]3161
[377]3162   if ($args{'way'} =~ m/all|desc/) {
3163      print "\n" if $interway++;
3164      # Switch description
[374]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
[379]3180      print $tb_desc->title()   unless $args{'no-header'};
3181      print $tb_desc->rule('-') unless $args{'no-header'};
[374]3182      print $tb_desc->body();
3183      $tb_desc->clear;
3184      }
3185
[261]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'}};
[4]3189
[377]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'});
[311]3205
[377]3206            }
3207         else {
3208            $tb_child->add($sw, $db_switch_output_port{$sw}, $arrow, '', 'router');
3209            }
[4]3210         }
[377]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
[379]3213      print $tb_child->title()   unless $args{'no-header'};
3214      print $tb_child->rule('-') unless $args{'no-header'};
[377]3215      print $tb_child->body(0, $tb_child->body_height() - 1);       # remove last fake line
3216      $tb_child->clear;
[4]3217      }
[311]3218
[377]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               }
[4]3240            }
3241         }
[378]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
[379]3244      print $tb_parent->title()   unless $args{'no-header'};
3245      print $tb_parent->rule('-') unless $args{'no-header'};
[378]3246      print $tb_parent->body(0, $tb_parent->body_height() - 1);      # remove last fake line
[377]3247      $tb_parent->clear;
[4]3248      }
[63]3249   return;
[4]3250   }
3251
[218]3252#---------------------------------------------------------------
[4]3253sub cmd_exportsw_dot {
[375]3254   my %args = (
3255      modulo   => 0,
3256      shift    => 1,
3257      @_);
[4]3258
[375]3259   my $graph_modulo = $args{'modulo'};
3260   my $graph_shift  = $args{'shift'};
3261
[44]3262   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
[63]3263
[261]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'};
[25]3270
[281]3271   my $invisible_node = 0; # Count number of invisible node
3272
[369]3273   my %db_building    = ();
[281]3274   my %db_switch_line = (); # Number of line drawed on a switch
[294]3275   for my $sw (values %db_switch_global) {
[261]3276      my ($building, $location) = split m/ \/ /xms, $sw->{'location'}, 2;
[2]3277      $db_building{$building} ||= {};
3278      $db_building{$building}->{$location} ||= {};
[369]3279      $db_building{$building}->{$location}{$sw->{'hostname'}} = 'y';
[290]3280
[281]3281      $db_switch_line{$sw} = 0;
[2]3282      }
[63]3283
3284
[2]3285   print "digraph G {\n";
[285]3286   print "rankdir=LR;\n";
[203]3287   #print "splines=polyline;\n";
[2]3288
[282]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";
[2]3291
[369]3292   my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime $timestamp;
[202]3293   $year += 1900;
3294   $mon++;
3295   my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
[288]3296   print "\"$date\" [label=\"MAP DATE\\n\\n$date\", color=white, fillcolor=black, shape=polygon, sides=14, style=filled, fontcolor=white];\n";
[286]3297   print "site -> \"$date\" [style=invis];\n";
[201]3298
[369]3299   my $b = 0;
[2]3300   for my $building (keys %db_building) {
3301      $b++;
[63]3302
[282]3303      print "\"building$b\" [label=\"$building\", color=black, fillcolor=gold, style=filled];\n";
3304      print "site -> \"building$b\" [len=2, color=firebrick];\n";
[2]3305
3306      my $l = 0;
3307      for my $loc (keys %{$db_building{$building}}) {
3308         $l++;
[63]3309
[282]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";
[291]3312         print "\"building$b\" -> \"location$b-$l\" [len=2, color=firebrick];\n";
[2]3313
3314         for my $sw (keys %{$db_building{$building}->{$loc}}) {
3315
[254]3316            my $peripheries = 1;
[369]3317            my $color       = 'lightblue';
[279]3318            if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
3319               $peripheries = 2;
[369]3320               $color       = "\"$color:$color\"";
[279]3321               }
[282]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";
[2]3323
[25]3324            my $swname  = $sw;
[309]3325               $swname .= q{\n-\n} . "$db_switch_global{$sw}->{'model'}" if exists $db_switch_global{$sw} and exists $db_switch_global{$sw}->{'model'};
[282]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";
[2]3329
3330
3331            for my $swport (keys %db_switch_connected_on_port) {
[248]3332               my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
[2]3333               next if not $sw_connect eq $sw;
3334               next if $port_connect eq $db_switch_output_port{$sw};
[254]3335               my $peripheries = 1;
[369]3336               my $color       = 'plum';
[280]3337               if ($port_connect =~ m/^(Trk|Br|Po)/) {
3338                  $peripheries = 2;
[369]3339                  $color       = "\"$color:$color\"";
[280]3340                  }
[282]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";
[290]3343
[283]3344               #$db_switch_line{$sw}++;
[282]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               #   }
[369]3353               }
[2]3354            }
3355         }
3356      }
3357
[369]3358   #   print "Switch output port and parent port connection\n";
3359   #   print "---------------------------------------------\n";
[2]3360   for my $sw (sort keys %db_switch_output_port) {
3361      if (exists $db_switch_parent{$sw}) {
[272]3362#         printf "   \"%s:%s\" -> \"%s:%s\"\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{'switch'}, $db_switch_parent{$sw}->{'port_id'};
[2]3363         }
3364      else {
[255]3365         my $style = 'solid';
[372]3366         my $color = 'black'; # navyblue
[278]3367         if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
3368            $style = 'bold';
3369            $color = "\"$color:invis:$color\"";
3370            }
[282]3371         printf "   \"%s:%s\" -> internet [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw};
[2]3372         }
3373      }
3374   print "\n";
3375
[306]3376   # shift graph between 1 or 2 when $graph_shift = 3
3377   my $graph_breaker = 1;
3378
[369]3379   #   print "Switch parent and children port inter-connection\n";
3380   #   print "------------------------------------------------\n";
[63]3381   for my $swport (sort keys %db_switch_connected_on_port) {
[248]3382      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
[2]3383      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
[254]3384         my $style = 'solid';
[372]3385         my $color = 'black'; # navyblue
[278]3386         if ($port_connect =~ m/^(Trk|Br|Po)/) {
3387            $style = 'bold';
3388            $color = "\"$color:invis:$color\"";
3389            }
[2]3390         if (exists $db_switch_output_port{$sw}) {
[282]3391            printf "   \"%s:%s\" -> \"%s:%s\" [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw}, $sw_connect, $port_connect;
3392
[289]3393            next if $graph_modulo == 0; # No shift (invisible nodes) in graph
[283]3394            $db_switch_line{$sw_connect}++;
[289]3395            if ($db_switch_line{$sw_connect} % $graph_modulo == 0) {
[282]3396               # Create invisible node
3397               $invisible_node++;
3398               my $invisible = '__Invisible_' . $invisible_node;
[289]3399               print  "   \"$invisible.a\" [shape=none, label=\"\"];\n";
3400               printf "   \"%s:%s\"  -> \"$invisible.a\" [style=invis];\n", $sw, $db_switch_output_port{$sw};
[306]3401               $graph_breaker++;
3402               if ($graph_shift == 2 or ($graph_shift == 3 and ($graph_breaker % 2) == 0)) {
[289]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                  }
[282]3412               }
[2]3413            }
3414         else {
[282]3415            printf "   \"%s\"   -> \"%s:%s\" [style=$style];\n", $sw, $sw_connect, $port_connect;
[2]3416            }
3417         }
3418      }
3419
[369]3420   print "}\n";
[63]3421   return;
[2]3422   }
3423
[218]3424################################################################
3425# documentation
3426################################################################
3427
[2]3428__END__
3429
3430=head1 NAME
3431
[219]3432klask - port and search manager for switches, map management
[2]3433
3434
[63]3435=head1 USAGE
[2]3436
[226]3437 klask version
3438 klask help
3439
[301]3440 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
[226]3441 klask exportdb [--format|-f txt|html]
[389]3442 klask removedb ipv4_addr* computer*
3443 klask insertdb --ip ipv4_addr --mac mac_addr --network vlan_name
[396]3444 klask cleandb  [--verbose|-v] [--day number_of_day] [--repair-dns]
[2]3445
[219]3446 klask updatesw [--verbose|-v]
[382]3447 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY] [--way all|desc|child|parent] [--no-header|-H]
[5]3448
[226]3449 klask searchdb [--kind|-k host|mac] computer [mac-address]
[2]3450 klask search   computer
[219]3451 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr
[2]3452
[226]3453 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
[69]3454
[330]3455 klask bad-vlan-id [--day|-d days_before_alert] [--format|-f txt|html]
[224]3456
[219]3457 klask enable  [--verbose|-v] switch port
3458 klask disable [--verbose|-v] switch port
3459 klask status  [--verbose|-v] switch port
[2]3460
[247]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
[238]3465 klask vlan-getname switch vlan-id
3466 klask vlan-list switch
[2]3467
[237]3468
[2]3469=head1 DESCRIPTION
3470
[246]3471Klask is a small tool to find where is connected a host in a big network
3472and on which VLAN.
[212]3473Klask mean search in brittany.
[200]3474No hight level protocol like CDL, LLDP are use.
3475Everything is just done with SNMP request on MAC address.
[2]3476
[212]3477Limitation : loop cannot be detected and could be problematic when the map is created (C<updatesw> method).
[200]3478If you use PVST or MSTP and create loop between VLAN,
[212]3479you have to use C<portignore> functionality on switch port to cut manually loop
[200]3480(see config file below).
3481
[212]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,
[389]3486to put this IPv4 as the fake ip for the switch.
[200]3487The MAC address associated will be use just for the map detection.
[212]3488The C<fake-ip> parameter is defined in the config file.
[200]3489
[219]3490Klask has now a web site dedicated for it: L<http://servforge.legi.grenoble-inp.fr/projects/klask>!
[2]3491
[6]3492
[2]3493=head1 COMMANDS
3494
[225]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).
[2]3499
3500=head2 search
3501
[226]3502 klask search   computer
3503
[200]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.
[2]3506
[227]3507=head2 search-mac-on-switch
[2]3508
[226]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.
[229]3512To search on all switch, you could put C<'*'> or C<all>.
3513The VLAN parameter could help.
[226]3514
3515
[2]3516=head2 enable
3517
[223]3518 klask enable  [--verbose|-v] switch port
[219]3519
[227]3520This command activate a port (or an agrregate bridge port) on a switch by SNMP.
[219]3521So you need to give the switch name and a port on the command line.
[225]3522See L</ABBREVIATION FOR PORT>.
[2]3523
[227]3524Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.
[2]3525
[219]3526
[2]3527=head2 disable
3528
[219]3529 klask disable [--verbose|-v] switch port
3530
[227]3531This command deactivate a port (or an agrregate bridge port) on a switch by SNMP.
[219]3532So you need to give the switch name and a port on the command line.
[225]3533See L</ABBREVIATION FOR PORT>.
[2]3534
[227]3535Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.
[2]3536
[219]3537
[2]3538=head2 status
3539
[223]3540 klask status  [--verbose|-v] switch port
[219]3541
[200]3542This command return the status of a port number on a switch by SNMP.
[219]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.
[225]3545See L</ABBREVIATION FOR PORT>.
[2]3546
[227]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.
[219]3550
[227]3551
[2]3552=head2 updatedb
3553
[301]3554 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
[223]3555
[226]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>).
[2]3560
[226]3561Option are not stable and could be use manually when you have a new kind of switch.
[223]3562Maybe some option will be transfered in a future C<checksw> command!
[2]3563
[227]3564The network parameter C<scan-mode> can have two values: C<active> or C<passive>.
3565By default, a network is C<active>.
[389]3566This means that an C<fping> command is done at the beginning on all the IPv4 of the network
[227]3567and the computers that was not detected in this pass, but where their Klask entry is less than one week,
[290]3568will have an C<arping>
[227]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
[301]3574At the beginning, the command verify that the switch map checksum is always valid.
3575Otherwise, a rebuild procedure will ne done automatically.
3576
[223]3577=head2 exportdb
[2]3578
[226]3579 klask exportdb [--format|-f txt|html]
[223]3580
[226]3581This command print the content of the computer database.
3582There is actually only two format : TXT and HTML.
3583By default, format is TXT.
[200]3584It's very easy to have more format, it's just need times...
[2]3585
[190]3586=head2 removedb
[2]3587
[389]3588 klask removedb ipv4_addr* computer*
[223]3589
[190]3590This command remove an entry in the database.
[389]3591There is only one kind of parameter, the IPv4 of the computers to be removed.
3592You can put as many IPv4 as you want...
[190]3593
[226]3594Computer DNS names are also a valid entry because a DNS resolver is executed at the beginning.
[190]3595
[389]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
[390]3602and the reverse DNS name resolution must be active for that IPv4.
[389]3603
[390]3604The date or timestamp of the entry will be C<number_of_day> in the past (by default 15, see L</cleandb>)
[389]3605from today or from the oldest entry with the same MAC-Address.
[390]3606This command could be use to add pseudo entry and help in the process to detect bad vlan configuration (see L</bad-vlan-id>).
[389]3607
[190]3608=head2 cleandb
3609
[396]3610 klask cleandb  [--verbose|-v] [--day number_of_day] [--repair-dns]
[190]3611
[223]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
[389]3616could have a float IPv4 when they are connected on VLAN 2.
[223]3617In the Klask database, the float DNS entries are less important.
[190]3618
[226]3619When reverse DNS has not been done by the past, option C<--repair-dns> force a reverse DNS check on all unkown host.
[223]3620
[5]3621=head2 updatesw
3622
[224]3623 klask updatesw [--verbose|-v]
3624
[200]3625This command build a map of your manageable switch on your network.
[226]3626The list of the switches must be given in the file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
[5]3627
[301]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).
[5]3630
[224]3631=head2 exportsw
[5]3632
[382]3633 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY] [--way all|desc|child|parent] [--no-header|-H]
[224]3634
[200]3635This command print the content of the switch database. There is actually two format.
[226]3636One is just TXT for terminal and the other is the DOT format from the graphviz environnement.
3637By default, format is TXT.
[5]3638
[380]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
[5]3651 klask exportsw --format dot > /tmp/map.dot
3652 dot -Tpng /tmp/map.dot > /tmp/map.png
3653
[307]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...
[5]3663
[224]3664=head2 ip-free
3665
[226]3666 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]
[224]3667
[389]3668This command return IPv4 address that was not use (detected by Klask) at this time.
[224]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).
[225]3672This parameter could also be define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
[224]3673
3674 default:
3675   days-to-death: 730
3676
[389]3677Computer that does not have the good IPv4 but takes a float one (see L</cleandb>) are taken into account.
[224]3678
3679
3680=head2 bad-vlan-id
3681
[330]3682 klask bad-vlan-id [--day|-d days_before_alert] [--format|-f txt|html]
[224]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>)
[389]3686and another prior trace where they had the good IPv4 (good DNS name).
[224]3687The computer must stay connected on a bad VLAN more than XX days (15 days by default) before alert.
[225]3688This parameter could also define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).
[224]3689
3690 default:
3691   days-before-alert: 15
3692
3693This functionality is not need if your switch use RADIUS 802.1X configuration...
3694
3695
[247]3696=head2 poe-enable
[224]3697
[247]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
[2]3736=head1 CONFIGURATION
3737
[219]3738Because Klask need many parameters, it's not possible actually to use command line parameters for everything.
[201]3739The configuration is done in a F</etc/klask/klask.conf> YAML file.
[200]3740This format have many advantage over XML, it's easier to read and to write !
[2]3741
3742Here an example, be aware with indent, it's important in YAML, do not use tabulation !
3743
3744 default:
3745   community: public
[220]3746   community-rw: private
[2]3747   snmpport: 161
[223]3748   float-regex: '(?^msx: ^float )'
[227]3749   scan-mode: active
[2]3750
3751 network:
[5]3752   labnet:
3753     ip-subnet:
3754       - add: 192.168.1.0/24
3755       - add: 192.168.2.0/24
3756     interface: eth0
[201]3757     vlan-id: 12
[5]3758     main-router: gw1.labnet.local
[2]3759
[5]3760   schoolnet:
3761     ip-subnet:
[227]3762       - add: 192.168.3.0/24
3763       - add: 192.168.4.0/24
[5]3764     interface: eth0.38
[201]3765     vlan-id: 13
[5]3766     main-router: gw2.schoolnet.local
[227]3767     scan-mode: passive
[5]3768
[227]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
[2]3777 switch:
3778   - hostname: sw1.klask.local
[227]3779     location: BatY / 1 floor / K004
[2]3780     portignore:
3781       - 1
3782       - 2
3783
3784   - hostname: sw2.klask.local
[227]3785     location: BatY / 2 floor / K203
[5]3786     type: HP2424
[2]3787     portignore:
3788       - 1
3789       - 2
[200]3790     fake-ip: 192.168.9.14
[2]3791
[227]3792   - hostname: sw3.klask.local
3793     location: BatY / 2 floor / K203
3794
[200]3795I think it's pretty easy to understand.
3796The default section can be overide in any section, if parameter mean something in theses sections.
[212]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.
[200]3799The switch section define your switch.
[219]3800You have to write the port number to ignore, this was important if your switchs are cascades
[212]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.
[2]3804
[222]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>.
[2]3811
[222]3812
[218]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
[246]3839In the case of use an aggregator port (Po, Tk, Br ...),
3840the real ports used are also return.
[218]3841
[246]3842
[259]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),
[364]3856J8692A(HP3500-24G), J4903A(HP2824), J4110A(HP8000M), JE074A(HP5120-24G), JE069A(HP5120-48G), JD377A(HP5500-24G), JD374A(HP5500-24F),
[406]3857J4121A(HP4000M), J9145A(HP2910-24G), J3298A(HP212M), J9625A(HP2620-24P), JH149A(HP5510-24F).
[259]3858
3859BayStack: BayStack 350T HW(BS350T)
3860
[393]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)
[259]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
[2]3874=head1 FILES
3875
[92]3876 /etc/klask/klask.conf
[100]3877 /var/lib/klask/klaskdb
3878 /var/lib/klask/switchdb
[2]3879
[219]3880
[2]3881=head1 SEE ALSO
3882
3883Net::SNMP, Net::Netmask, Net::CIDR::Lite, NetAddr::IP, YAML
3884
[219]3885=over
[2]3886
[220]3887=item * L<Web site|http://servforge.legi.grenoble-inp.fr/projects/klask>
[219]3888
[220]3889=item * L<Online Manual|http://servforge.legi.grenoble-inp.fr/pub/klask/klask.html>
[219]3890
3891=back
3892
3893
[2]3894=head1 VERSION
3895
[36]3896$Id: klask 412 2020-01-28 15:56:40Z g7moreau $
[2]3897
3898
3899=head1 AUTHOR
3900
[399]3901Written by Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>, Grenoble - France
[2]3902
[368]3903=head1 SPECIAL THANKS
[2]3904
[368]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
[393]3911Kevin Reverchon, Olivier De Marchi, Patrick Begou, Herve Colasuonno, David Gras.
[368]3912
3913
[63]3914=head1 LICENSE AND COPYRIGHT
[2]3915
[399]3916License GNU GPL version 2 or later and Perl equivalent
[45]3917
[408]3918Copyright (C) 2005-2020 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>.
Note: See TracBrowser for help on using the repository browser.