source: trunk/klask @ 399

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