source: trunk/klask @ 389

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