#!/usr/bin/perl -w
#
# Copyright (C) 2005-2016 Gabriel Moreau
#
# $Id: klask 178 2016-08-24 15:55:17Z g7moreau $

use strict;
use warnings;
use version; our $VERSION = qv('0.5.6');

use Readonly;
use FileHandle;
use Net::SNMP;
#use YAML;
use YAML::Syck;
use Net::Netmask;
use Net::CIDR::Lite;
use NetAddr::IP;
use Getopt::Long qw(GetOptions);
use Socket;
use List::Util 'shuffle';

# apt-get install snmp fping libnet-cidr-lite-perl libnet-netmask-perl libnet-snmp-perl libnetaddr-ip-perl libyaml-perl
# libcrypt-des-perl libcrypt-hcesha-perl libdigest-hmac-perl 
# arping net-tools fping bind9-host arpwatch

my $KLASK_VAR      = '/var/lib/klask';
my $KLASK_CFG_FILE = '/etc/klask/klask.conf';
my $KLASK_DB_FILE  = "$KLASK_VAR/klaskdb";
my $KLASK_SW_FILE  = "$KLASK_VAR/switchdb";

test_running_environnement();

my $KLASK_CFG = YAML::Syck::LoadFile("$KLASK_CFG_FILE");

my %DEFAULT = %{ $KLASK_CFG->{default} };
my @SWITCH  = @{ $KLASK_CFG->{switch}  };

my %switch_level = ();
my %SWITCH_DB    = ();
LEVEL_OF_EACH_SWITCH:
for my $sw (@SWITCH){
   $switch_level{$sw->{hostname}} = $sw->{level} || $DEFAULT{switch_level}  || 2;
   $SWITCH_DB{$sw->{hostname}} = $sw;
   }
@SWITCH = reverse sort { $switch_level{$a->{hostname}} <=> $switch_level{$b->{hostname}} } @{$KLASK_CFG->{switch}};

my %SWITCH_PORT_COUNT = ();

my %CMD_DB = (
   'help'                 => \&cmd_help,
   'version'              => \&cmd_version,
   'exportdb'             => \&cmd_exportdb,
   'updatedb'             => \&cmd_updatedb,
   'searchdb'             => \&cmd_searchdb,
   'removedb'             => \&cmd_removedb,
   'cleandb'              => \&cmd_cleandb,
   'search'               => \&cmd_search,
   'enable'               => \&cmd_enable,
   'disable'              => \&cmd_disable,
   'status'               => \&cmd_status,
   'updatesw'             => \&cmd_updatesw,
   'exportsw'             => \&cmd_exportsw,
   'iplocation'           => \&cmd_ip_location,
   'ip-free'              => \&cmd_ip_free,
   'search-mac-on-switch' => \&cmd_search_mac_on_switch,
   'bad-vlan-id'          => \&cmd_bad_vlan_id,
   'set-vlan-port'        => \&cmd_set_vlan_port,
   'get-vlan-port'        => \&cmd_get_vlan_port,
   'set-vlan-name'        => \&cmd_set_vlan_name,
   'get-vlan-name'        => \&cmd_get_vlan_name,
   'rebootsw'             => \&cmd_rebootsw,
   );

Readonly my %INTERNAL_PORT_MAP => (
   0 => 'A',
   1 => 'B',
   2 => 'C',
   3 => 'D',
   4 => 'E',
   5 => 'F',
   6 => 'G',
   7 => 'H',
   );
Readonly my %INTERNAL_PORT_MAP_REV => reverse %INTERNAL_PORT_MAP;

Readonly my %SWITCH_KIND => (
   # HP
   J3299A           => { type => 1, model => 'HP224M',         match => 'HP J3299A ProCurve Switch 224M'       },
   J4120A           => { type => 1, model => 'HP1600M',        match => 'HP J4120A ProCurve Switch 1600M'      },
   J9029A           => { type => 1, model => 'HP1800-8G',      match => 'PROCURVE J9029A'                      },
   J9449A           => { type => 1, model => 'HP1810-8G',      match => 'HP ProCurve 1810G - 8 GE'             },
   J4093A           => { type => 1, model => 'HP2424M',        match => 'HP J4093A ProCurve Switch 2424M'      },
   J9279A           => { type => 1, model => 'HP2510G-24',     match => 'ProCurve J9279A Switch 2510G-24'      },
   J9280A           => { type => 1, model => 'HP2510G-48',     match => 'ProCurve J9280A Switch 2510G-48'      },
   J4813A           => { type => 1, model => 'HP2524',         match => 'HP J4813A ProCurve Switch 2524'       },
   J4900A           => { type => 1, model => 'HP2626A',        match => 'HP J4900A ProCurve Switch 2626'       },
   J4900B           => { type => 1, model => 'HP2626B',        match => 'J4900B.+?Switch 2626'                 }, # ProCurve J4900B Switch 2626 # HP J4900B ProCurve Switch 2626
   J4899B           => { type => 1, model => 'HP2650',         match => 'ProCurve J4899B Switch 2650'          },
   J9021A           => { type => 1, model => 'HP2810-24G',     match => 'ProCurve J9021A Switch 2810-24G'      },
   J9022A           => { type => 1, model => 'HP2810-48G',     match => 'ProCurve J9022A Switch 2810-48G'      },
   J8692A           => { type => 1, model => 'HP3500-24G',     match => 'J8692A Switch 3500yl-24G'             },
   J4903A           => { type => 1, model => 'HP2824',         match => 'J4903A.+?Switch 2824,'                },
   J4110A           => { type => 1, model => 'HP8000M',        match => 'HP J4110A ProCurve Switch 8000M'      },
   JE074A           => { type => 2, model => 'HP5120-24G',     match => 'HP Comware.+?A5120-24G EI Switch'     },
   JE069A           => { type => 2, model => 'HP5120-48G',     match => 'HP Comware.+?A5120-48G EI Switch'     },
   JD377A           => { type => 2, model => 'HP5500-24G',     match => 'HP Comware.+?A5500-24G EI Switch'     },
   JD374A           => { type => 2, model => 'HP5500-24F',     match => 'HP Comware.+?A5500-24G-SFP EI Switch' },
   # BayStack
   BS350T           => { type => 1, model => 'BS350T',         match => 'BayStack 350T HW'                     },
   # Nexans
   N3483G           => { type => 2, model => 'NA3483-6G',      match => 'GigaSwitch V3 TP SFP-I 48V ES3'       },
   # DELL
   N2048            => { type => 2, model => 'DN2048',         match => 'Dell Networking N2048,'               },
   N4064F           => { type => 2, model => 'DN4064F',        match => 'Dell Networking N4064F,'              },
   # 3COM
   'H3C5500'        => { type => 1, model => 'H3C5500',        match => 'H3C S5500-SI Series'                  },
   '3C17203'        => { type => 1, model => '3C17203',        match => '3Com SuperStack 3 24-Port'            },
   '3C17204'        => { type => 1, model => '3C17204',        match => '3Com SuperStack 3 48-Port'            },
   '3CR17562-91'    => { type => 1, model => '3CR17562-91',    match => '3Com Switch 4500 50-Port'             },
   '3CR17255-91'    => { type => 1, model => '3CR17255-91',    match => '3Com Switch 5500G-EI 48-Port'         },
   '3CR17251-91'    => { type => 1, model => '3CR17251-91',    match => '3Com Switch 5500G-EI 48-Port'         },
   '3CR17571-91'    => { type => 1, model => '3CR17571-91',    match => '3Com Switch 4500 PWR 26-Port'         },
   '3CRWX220095A'   => { type => 1, model => '3CRWX220095A',   match => '3Com Wireless LAN Controller'         },
   '3CR17254-91'    => { type => 1, model => '3CR17254-91',    match => '3Com Switch 5500G-EI 24-Port'         },
   '3CRS48G-24S-91' => { type => 1, model => '3CRS48G-24S-91', match => '3Com Switch 4800G 24-Port'            },
   '3CRS48G-48S-91' => { type => 1, model => '3CRS48G-48S-91', match => '3Com Switch 4800G 48-Port'            },
   '3C17708'        => { type => 1, model => '3C17708',        match => '3Com Switch 4050'                     },
   '3C17709'        => { type => 1, model => '3C17709',        match => '3Com Switch 4060'                     },
   '3C17707'        => { type => 1, model => '3C17707',        match => '3Com Switch 4070'                     },
   '3CR17258-91'    => { type => 1, model => '3CR17258-91',    match => '3Com Switch 5500G-EI 24-Port SFP'     },
   '3CR17181-91'    => { type => 1, model => '3CR17181-91',    match => '3Com Switch 5500-EI 28-Port FX'       },
   '3CR17252-91'    => { type => 1, model => '3CR17252-91',    match => '3Com Switch 5500G-EI PWR 24-Port'     },
   '3CR17253-91'    => { type => 1, model => '3CR17253-91',    match => '3Com Switch 5500G-EI PWR 48-Port'     },
   '3CR17250-91'    => { type => 1, model => '3CR17250-91',    match => '3Com Switch 5500G-EI 24-Port'         },
   '3CR17561-91'    => { type => 1, model => '3CR17561-91',    match => '3Com Switch 4500 26-Port'             },
   '3CR17572-91'    => { type => 1, model => '3CR17572-91',    match => '3Com Switch 4500 PWR 50-Port'         },
   '3C17702-US'     => { type => 1, model => '3C17702-US',     match => '3Com Switch 4900 SX'                  },
   '3C17700'        => { type => 1, model => '3C17700',        match => '3Com Switch 4900'                     },
   );

Readonly my %OID_NUMBER => (
   sysDescription  => '1.3.6.1.2.1.1.1.0',
   sysName         => '1.3.6.1.2.1.1.5.0',
   sysContact      => '1.3.6.1.2.1.1.4.0',
   sysLocation     => '1.3.6.1.2.1.1.6.0',
   searchPort1     => '1.3.6.1.2.1.17.4.3.1.2',       # BRIDGE-MIB (802.1D).
   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
   vlanPortDefault => '1.3.6.1.2.1.17.7.1.4.5.1.1',   # dot1qPvid
   vlanStatus      => '1.3.6.1.2.1.17.7.1.4.3.1.5',   # integer 4 Create, 6 Destroy
   vlanName        => '1.3.6.1.2.1.17.7.1.4.3.1.1',   # string
   hpicfReset      => '1.3.6.1.4.1.11.2.14.11.1.4.1', # HP reboot switch
   ifIndex         => '1.3.6.1.2.1.17.1.4.1.2',       # dot1dBasePortIfIndex - Interface index redirection
   ifName          => '1.3.6.1.2.1.31.1.1.1.1',       # Interface name (give port number)
   );

Readonly 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;
Readonly my $RE_IPv4_ADDRESS => qr{ [0-9]{1,3} \. [0-9]{1,3} \. [0-9]{1,3} \. [0-9]{1,3} }xms;

Readonly my $RE_FLOAT_HOSTNAME => qr{ ^float }xms;


################
# principal
################

my $cmd = shift @ARGV || 'help';
if (defined $CMD_DB{$cmd}) {
   $CMD_DB{$cmd}->(@ARGV);
   }
else {
   print {*STDERR} "klask: command $cmd not found\n\n";
   $CMD_DB{help}->();
   exit 1;
   }

exit;

sub test_running_environnement {
   die "Configuration file $KLASK_CFG_FILE does not exists. Klask need it !\n" if not -e "$KLASK_CFG_FILE";
   die "Var folder $KLASK_VAR does not exists. Klask need it !\n"              if not -d "$KLASK_VAR";
   return;
   }

sub test_switchdb_environnement {
   die "Switch database $KLASK_SW_FILE does not exists. Launch updatesw before this command !\n" if not -e "$KLASK_SW_FILE";
   return;
   }

sub test_maindb_environnement {
   die "Main database $KLASK_DB_FILE does not exists. Launch updatedb before this command !\n" if not -e "$KLASK_DB_FILE";
   return;
   }

###
# fast ping dont l'objectif est de remplir la table arp de la machine
sub fast_ping {
   # Launch this command without waiting...
   system "fping -q -c 1 @_ >/dev/null 2>&1 &";
   return;
   }

sub shell_command {
   my $cmd = shift;

   my $fh     = new FileHandle;
   my $result = '';
   open $fh, q{-|}, "LANG=C $cmd" or die "Can't exec $cmd\n";
   $result .= <$fh>;
   close $fh;
   chomp $result;
   return $result;
   }

###
# donne l'@ ip, dns, arp en fonction du dns OU de l'ip
sub resolve_ip_arp_host {
   my $param_ip_or_host = shift;
   my $interface = shift || q{*};
   my $type      = shift || q{fast};

   my %ret = (
      hostname_fq  => 'unknow',
      ipv4_address => '0.0.0.0',
      mac_address  => 'unknow',
      );

   # perl -MSocket -E 'say inet_ntoa(scalar gethostbyname("tech7meylan.hmg.inpg.fr"))'
   my $packed_ip = scalar gethostbyname($param_ip_or_host);
   return %ret if not defined $packed_ip;
   $ret{ipv4_address} = inet_ntoa($packed_ip);

   # perl -MSocket -E 'say scalar gethostbyaddr(inet_aton("194.254.66.240"), AF_INET)'
   my $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET);
   $ret{hostname_fq} = $hostname_fq if defined $hostname_fq;

   # my $cmd = q{grep  -he '\b} . $param_ip_or_host . q{\b' } . "/var/lib/arpwatch/$interface.dat | sort -rn -k 3,3 | head -1";
   #my $cmd = q{grep  -he '\b} . $ret{ipv4_address} . q{\b' } . "/var/lib/arpwatch/$interface.dat | sort -rn -k 3,3 | head -1";
   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';
   #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

   my $cmd_arpwatch = shell_command $cmd;
   #my ($arp, $ip, $timestamp, $host) = split m/ \s+ /xms, $cmd_arpwatch;
   my ($interface2, $arp, $ip, $timestamp, $host) = split m/ \s+ /xms, $cmd_arpwatch;

   $ret{interface}    = $interface2 || $interface;
   $ret{mac_address}  = $arp       if $arp;
   $ret{timestamp}    = $timestamp if $timestamp;

   my $nowtimestamp = time;

   if ( $type eq 'fast' and ( not defined $timestamp or $timestamp < ( $nowtimestamp - 45 * 60 ) ) ) { # 45 min
      $ret{mac_address} = 'unknow';
      return %ret;
      }

   # resultat de la commande arp
   # tech7meylan.hmg.inpg.fr (194.254.66.240) at 00:14:22:45:28:A9 [ether] on eth0
   # sw2-batF0-legi.hmg.priv (192.168.22.112) at 00:30:c1:76:9c:01 [ether] on eth0.37
   my $cmd_arp  = shell_command "arp -a $param_ip_or_host -i $ret{interface}";
   if ( $cmd_arp =~ m{ (\S*) \s \( ( $RE_IPv4_ADDRESS ) \) \s at \s ( $RE_MAC_ADDRESS ) }xms ) {
      ( $ret{hostname_fq}, $ret{ipv4_address}, $ret{mac_address} )  = ($1, $2, $3);
      }

   # Normalize MAC Address
   if ($ret{mac_address} ne 'unknow') {
      my @paquets = ();
      for ( split m/ : /xms, $ret{mac_address} ) {
         my @chars = split m//xms, uc "00$_";
         push @paquets, "$chars[-2]$chars[-1]";
         }
      $ret{mac_address} = join q{:}, @paquets;
      }

   return %ret;
   }

# Find Surname of a switch
sub get_switch_model {
   my $sw_snmp_description = shift || 'unknow';
   $sw_snmp_description =~ s/[\n\r]/ /g;

   for my $sw_kind (keys %SWITCH_KIND) {
      next if not $sw_snmp_description =~ m/$SWITCH_KIND{$sw_kind}->{match}/ms; # option xms break search, why ?

      return $SWITCH_KIND{$sw_kind}->{model};
      }

   return $sw_snmp_description;
   }

###
# va rechercher le nom des switchs pour savoir qui est qui
sub init_switch_names {
   my ($verbose, $verb_description, $check_hostname, $check_location) = @_;

   printf "%-26s                %-25s %s\n",'Switch','Description','Type' if $verbose;
   print "------------------------------------------------------------------------------\n" if $verbose;

   INIT_EACH_SWITCH:
   for my $sw (@SWITCH) {
      my %session = ( -hostname   => $sw->{hostname} );
         $session{-version} = $sw->{version}   || 1;
         $session{-port}    = $sw->{snmpport}  || $DEFAULT{snmpport}  || 161;
         if (exists $sw->{version} and $sw->{version} eq '3') {
            $session{-username} = $sw->{username} || 'snmpadmin';
            }
         else {
            $session{-community} = $sw->{community} || $DEFAULT{community} || 'public';
            }

      $sw->{local_session} = \%session;

      my ($session, $error) = Net::SNMP->session( %{$sw->{local_session}} );
      print "$error \n" if $error;

      my $result = $session->get_request(
         -varbindlist => [
            $OID_NUMBER{sysDescription},
            $OID_NUMBER{sysName},
            $OID_NUMBER{sysContact},
            $OID_NUMBER{sysLocation},
            ]
         );
      $sw->{description} = $result->{$OID_NUMBER{sysName}} || $sw->{hostname};
      $sw->{model} = get_switch_model($result->{$OID_NUMBER{sysDescription}});
      if ($verb_description) {
         my $desc = $result->{$OID_NUMBER{sysDescription}};
         $desc =~ s/[\n\r]/ /g;
         print "   description: $desc\n"
         }
      if ($check_hostname) {
         my ($hostname) = split /\./, $sw->{hostname}, 2;
         print " $hostname - error internal hostname: $sw->{hostname}\n" if $result->{$OID_NUMBER{sysName}} ne $hostname;
         }
      if ($check_location) {
         my $location = $result->{$OID_NUMBER{sysLocation}};
         $location =~ s/^"(.+)"$/$1/;
         print " $sw->{hostname} - error location: '$location' -> '$sw->{location}'\n" if $location ne $sw->{location};
         }
      #$sw->{location} = $result->{"1.3.6.1.2.1.1.6.0"} || $sw->{hostname};
      #$sw->{contact} = $result->{"1.3.6.1.2.1.1.4.0"} || $sw->{hostname};
      $session->close;

      # Ligne à virer car on récupère maintenant le modèle du switch
      my ($desc, $type) = split m/ : /xms, $sw->{description}, 2;
      printf "%-26s 0--------->>>> %-25s %s\n", $sw->{hostname}, $desc, $sw->{model} if $verbose;
      }

   print "\n" if $verbose;
   return;
   }

###
# convertit l'hexa (uniquement 2 chiffres) en decimal
sub digit_hex_to_dec {
   #00:0F:1F:43:E4:2B
   my $car = '00' . uc shift;

   return '00' if $car eq '00UNKNOW';
   my %table = (
      '0'=>'0',  '1'=>'1',  '2'=>'2',  '3'=>'3',  '4'=>'4',
      '5'=>'5',  '6'=>'6',  '7'=>'7',  '8'=>'8',  '9'=>'9',
      'A'=>'10', 'B'=>'11', 'C'=>'12', 'D'=>'13', 'E'=>'14', 'F'=>'15',
      );
   my @chars = split m//xms, $car;
   return $table{$chars[-2]}*16 + $table{$chars[-1]};
   }

#--------------------------------------------------------------------------------

sub normalize_mac_address {
   my $mac_address = shift;

   # D07E-28D1-7AB8 or d07e28-d17ab8
   if ($mac_address =~ m{^ (?: [0-9A-Fa-f]{4} -){2} [0-9A-Fa-f]{4} $}xms
      or $mac_address =~ m{^ [0-9A-Fa-f]{6} - [0-9A-Fa-f]{6} $}xms) {
      $mac_address =~ s/-//g;
      return join q{:}, unpack('(A2)*', uc($mac_address));
      }

   return join q{:}, map { substr( uc("00$_"), -2) } split m/ [:-] /xms, $mac_address;
   }

#--------------------------------------------------------------------------------
# convertit l'@ mac en decimal
sub mac_address_hex_to_dec {
   #00:0F:1F:43:E4:2B
   my $mac_address = shift;

   my @paquets = split m/ : /xms, $mac_address;
   my $return = q{};
   for (@paquets) {
      $return .= q{.} . digit_hex_to_dec($_);
      }
   return $return;
   }

###
# va rechercher le port et le switch sur lequel est la machine
sub find_switch_port {
   my $mac_address     = shift;
   my $switch_proposal = shift || q{};
   my $vlan_id = shift || 0;

   my %ret;
   $ret{switch_description} = 'unknow';
   $ret{switch_port} = '0';

   return %ret if $mac_address eq 'unknow';;

   my @switch_search = @SWITCH;
   if ($switch_proposal ne q{}) {
      for my $sw (@SWITCH) {
         next if $sw->{hostname} ne $switch_proposal;
         unshift @switch_search, $sw;
         last;
         }
      }

   my $research1 = $OID_NUMBER{searchPort1} . mac_address_hex_to_dec($mac_address);
   my $research2 = $OID_NUMBER{searchPort2} .'.'. $vlan_id . mac_address_hex_to_dec($mac_address);

   LOOP_ON_SWITCH:
   for my $sw (@switch_search) {
      my ($session, $error) = Net::SNMP->session( %{$sw->{local_session}} );
      print "$error \n" if $error;

      my $result = $session->get_request(
         -varbindlist => [$research1]
         );
      if (not defined $result) {
         $result = $session->get_request(
            -varbindlist => [$research2]
            );
         $result->{$research1} = $result->{$research2} if defined $result;
         }

      if (not (defined $result and $result->{$research1} ne 'noSuchInstance')) {
         $session->close;
         next LOOP_ON_SWITCH;
         }

      my $swport_num = $result->{$research1};
      my $swport_hr = get_human_readable_port($sw->{model}, snmp_get_swithport_hr($session, $swport_num));

      $session->close;

      # IMPORTANT !!
      # ceci empeche la detection sur certains port ...
      # en effet les switch sont relies entre eux par un cable reseau et du coup
      # tous les arp de toutes les machines sont presentes sur ces ports (ceux choisis ici sont les miens)
      # cette partie est a ameliore, voir a configurer dans l'entete
      # 21->24 45->48
      SWITCH_PORT_IGNORE:
      for my $portignore (@{$sw->{portignore}}) {
         next LOOP_ON_SWITCH if $swport_hr eq $portignore;
         }

      $ret{switch_hostname}    = $sw->{hostname};
      $ret{switch_description} = $sw->{description};
      $ret{switch_port}        = $swport_num;
      $ret{switch_port_hr}     = $swport_hr; # human readable

      last LOOP_ON_SWITCH;
      }
   return %ret;
   }

###
# va rechercher les port et les switch sur lequel est la machine
sub find_all_switch_port {
   my $mac_address = shift;
   my $vlan_id     = shift || 0;

   my $ret = {};

   return $ret if $mac_address eq 'unknow';

   my $research1 = $OID_NUMBER{searchPort1} . mac_address_hex_to_dec($mac_address);
   my $research2 = $OID_NUMBER{searchPort2} .'.'. $vlan_id . mac_address_hex_to_dec($mac_address);
   LOOP_ON_ALL_SWITCH:
   for my $sw (@SWITCH) {
      my ($session, $error) = Net::SNMP->session( %{$sw->{local_session}} );
      print "$error \n" if $error;

      my $result = $session->get_request(
         -varbindlist => [$research1]
         );
      if (not defined $result) {
         $result = $session->get_request(
            -varbindlist => [$research2]
            );
         $result->{$research1} = $result->{$research2} if defined $result;
         }

      if (defined $result and $result->{$research1} ne 'noSuchInstance') {
         my $swport_num = $result->{$research1};
         my $swport_hr = get_human_readable_port($sw->{model}, snmp_get_swithport_hr($session, $swport_num));

         $ret->{$sw->{hostname}} = {};
         $ret->{$sw->{hostname}}{hostname}    = $sw->{hostname};
         $ret->{$sw->{hostname}}{description} = $sw->{description};
         $ret->{$sw->{hostname}}{port}        = $swport_num;
         $ret->{$sw->{hostname}}{port_hr}     = $swport_hr;

#         $SWITCH_PORT_COUNT{$sw->{hostname}}->{$swport}++;
         }

      $session->close;
      }
   return $ret;
   }

sub get_list_network {

   return keys %{$KLASK_CFG->{network}};
   }

sub get_current_interface {
   my $vlan_name = shift;

   return $KLASK_CFG->{network}{$vlan_name}{interface};
   }

sub get_current_vlan_id {
   my $vlan_name = shift;

   return $KLASK_CFG->{network}{$vlan_name}{'vlan-id'};
   }

sub get_current_vlan_name_for_interface {
   my $interface = shift;

   for my $vlan_name (keys %{$KLASK_CFG->{network}}) {
      next if $KLASK_CFG->{network}{$vlan_name}{interface} ne $interface;
      return $vlan_name;
      }
   }

###
# liste l'ensemble des adresses ip d'un réseau
sub get_list_ip {
   my @vlan_name = @_;

   my $cidrlist = Net::CIDR::Lite->new;

   for my $net (@vlan_name) {
      my @line  = @{$KLASK_CFG->{network}{$net}{'ip-subnet'}};
      for my $cmd (@line) {
         for my $method (keys %{$cmd}){
            $cidrlist->add_any($cmd->{$method}) if $method eq 'add';
            }
         }
      }

   my @res = ();

   for my $cidr ($cidrlist->list()) {
      my $net = new NetAddr::IP $cidr;
      for my $ip (@{$net}) {
         $ip =~ s{ /32 }{}xms;
         push @res,  $ip;
         }
      }

   return @res;
   }

# liste l'ensemble des routeurs du réseau
sub get_list_main_router {
   my @vlan_name = @_;

   my @res = ();

   for my $net (@vlan_name) {
      push @res, $KLASK_CFG->{network}{$net}{'main-router'};
      }

   return @res;
   }

sub get_human_readable_port {
   my $sw_model = shift;
   my $sw_port  = shift;

   # Not need anymore
   # get port name by snmp
   return $sw_port;

   if ($sw_model eq 'HP8000M') {

      my $reste = (($sw_port - 1) % 8) + 1;
      my $major = int (($sw_port - 1) / 8);
      return "$INTERNAL_PORT_MAP{$major}$reste";
      }

   if ($sw_model eq 'HP2424M') {
      if ($sw_port > 24) {
         
         my $reste = $sw_port - 24;
         return "A$reste";
         }
      }

   if ($sw_model eq 'HP1600M') {
      if ($sw_port > 16) {
         
         my $reste = $sw_port - 16;
         return "A$reste";
         }
      }

   if ($sw_model eq 'HP2810-48G' or $sw_model eq 'HP2810-24G') {
      if ($sw_port > 48) {
         
         my $reste = $sw_port - 48;
         return "Trk$reste";
         }
      }

   if ($sw_model eq 'HP3500-24G') {
      if ($sw_port > 289) {
         
         my $reste = $sw_port - 289;
         return "Trk$reste";
         }
      }

   return $sw_port;
   }

sub get_numerical_port {
   my $sw_model = shift;
   my $sw_port  = shift;

   if ($sw_model eq 'HP8000M') {

      my $letter = substr $sw_port, 0, 1;
      my $reste =  substr $sw_port, 1;

      return $INTERNAL_PORT_MAP_REV{$letter} * 8 + $reste;
      }

   if ($sw_model eq 'HP2424M') {
      if ($sw_port =~ m/^A/xms ) {

         my $reste =  substr $sw_port, 1;

         return 24 + $reste;
         }
      }

   if ($sw_model eq 'HP1600M') {
      if ($sw_port =~ m/^A/xms ) {

         my $reste =  substr $sw_port, 1;

         return 16 + $reste;
         }
      }

   if ($sw_model eq 'HP2810-48G' or $sw_model eq 'HP2810-24G') {
      if ($sw_port =~ m/^Trk/xms ) {

         my $reste =  substr $sw_port, 3;

         return 48 + $reste;
         }
      }

   if ($sw_model eq 'HP3500-24G') {
      if ($sw_port =~ m/^Trk/xms ) {

         my $reste =  substr $sw_port, 3;

         return 289 + $reste;
         }
      }

   return $sw_port;
   }

sub get_port_human_readable_short {
   my $sw_port_hr  = shift;

   $sw_port_hr =~ s/^Bridge-Aggregation/Br/i;
   $sw_port_hr =~ s/^Port-Channel/Po/i;
   $sw_port_hr =~ s/^Forty-?GigabitEthernet/Fo/i;
   $sw_port_hr =~ s/^Ten-?GigabitEthernet/Te/i;
   $sw_port_hr =~ s/^GigabitEthernet/Gi/i;
   $sw_port_hr =~ s/^FastEthernet/Fa/i;

   return $sw_port_hr;
   }

sub snmp_get_swithport_hr {
   my ($snmp_session, $swport) = @_;
   
   my $research_index = $OID_NUMBER{ifIndex} .'.'. $swport;
   my $result_index = $snmp_session->get_request(
      -varbindlist => [$research_index]
      );
   my $swifindex = $swport;
   $swifindex = $result_index->{$research_index} if defined $result_index;

   my $research_hr = $OID_NUMBER{ifName} .'.'. $swifindex;
   my $result_hr = $snmp_session->get_request(
      -varbindlist => [$research_hr]
      );
   my $swport_hr = $swport;
   $swport_hr = get_port_human_readable_short($result_hr->{$research_hr}) if defined $result_hr;
   return $swport_hr;
   }

# Load computer database
sub computerdb_load {
   my $computerdb = YAML::Syck::LoadFile("$KLASK_DB_FILE");

   LOOP_ON_IP_ADDRESS:
   for my $ip (keys %{$computerdb}) {

      next LOOP_ON_IP_ADDRESS if exists $computerdb->{$ip}{switch_port_hr};
      
      $computerdb->{$ip}{switch_port_hr} = $computerdb->{$ip}{switch_port};
      }

   return $computerdb;
   }

################
# Les commandes
################

sub cmd_help {

print <<'END';
klask - ports manager and finder for switch

 klask version

 klask updatedb
 klask exportdb --format [txt|html]
 klask removedb computer*
 klask cleandb  --day number_of_day --verbose

 klask updatesw
 klask exportsw --format [txt|dot]

 klask searchdb computer
 klask search   computer
 klask search-mac-on-switch switch mac_addr

 klask ip-free --day number_of_day --format [txt|html] [vlan_name]

 klask bad-vlan-id

 klask enable  switch port
 klask disable switch port
 klask status  switch port
END
   return;
   }

sub cmd_version {

print <<'END';
Klask - ports manager and finder for switch
Copyright (C) 2005-2016 Gabriel Moreau

END
   print ' $Id: klask 178 2016-08-24 15:55:17Z g7moreau $'."\n";
   return;
   }

sub cmd_search {
   my @computer = @_;

   init_switch_names();    #nomme les switchs
   fast_ping(@computer);

   LOOP_ON_COMPUTER:
   for my $clientname (@computer) {
      my %resol_arp = resolve_ip_arp_host($clientname);          #resolution arp
      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{interface});
      my $vlan_id   = get_current_vlan_id($vlan_name);
      my %where     = find_switch_port($resol_arp{mac_address}, '', $vlan_id); #retrouve l'emplacement

      next LOOP_ON_COMPUTER if $where{switch_description} eq 'unknow' or $resol_arp{hostname_fq} eq 'unknow' or $resol_arp{mac_address} eq 'unknow';

      printf '%-22s %2s %-30s %-15s %18s',
         $where{switch_hostname},
         $where{switch_port_hr},
         $resol_arp{hostname_fq},
         $resol_arp{ipv4_address},
         $resol_arp{mac_address}."\n";
      }
   return;
   }

sub cmd_searchdb {
   my @ARGV  = @_;

   my $kind;

   GetOptions(
      'kind=s'   => \$kind,
      );

   my %possible_search = (
      host  => \&cmd_searchdb_host,
      mac   => \&cmd_searchdb_mac,
      );

   $kind = 'host' if not defined $possible_search{$kind};

   $possible_search{$kind}->(@ARGV);
   return;
   }


sub cmd_searchdb_host {
   my @computer = @_;

   fast_ping(@computer);
   my $computerdb = computerdb_load();

   LOOP_ON_COMPUTER:
   for my $clientname (@computer) {
      my %resol_arp = resolve_ip_arp_host($clientname);      #resolution arp
      my $ip = $resol_arp{ipv4_address};

      next LOOP_ON_COMPUTER unless exists $computerdb->{$ip};

      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
      $year += 1900;
      $mon++;
      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;

      printf "%-22s %2s %-30s %-15s %-18s %s\n",
         $computerdb->{$ip}{switch_hostname},
         $computerdb->{$ip}{switch_port_hr},
         $computerdb->{$ip}{hostname_fq},
         $ip,
         $computerdb->{$ip}{mac_address},
         $date;
      }
   return;
   }

sub cmd_searchdb_mac {
   my @mac = map { normalize_mac_address($_) } @_;

   my $computerdb = computerdb_load();

   LOOP_ON_MAC:
   for my $mac (@mac) {
      LOOP_ON_COMPUTER:
      for my $ip (keys %{$computerdb}) {
         next LOOP_ON_COMPUTER if $mac ne $computerdb->{$ip}{mac_address};
 
         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
         $year += 1900;
         $mon++;
         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;

         printf "%-22s %2s %-30s %-15s %-18s %s\n",
            $computerdb->{$ip}{switch_hostname},
            $computerdb->{$ip}{switch_port_hr},
            $computerdb->{$ip}{hostname_fq},
            $ip,
            $computerdb->{$ip}{mac_address},
            $date;
         #next LOOP_ON_MAC;
         }

      }
   return;
   }

sub cmd_updatedb {
   @ARGV = @_;

   my ($verbose, $verb_description, $check_hostname, $check_location);

   GetOptions(
      'verbose|v'          => \$verbose,
      'verb-description|d' => \$verb_description,
      'chk-hostname|h'     => \$check_hostname,
      'chk-location|l'     => \$check_location,
      );

   my @network = @ARGV;
      @network = get_list_network() if not @network;

   test_switchdb_environnement();

   my $computerdb = {};
      $computerdb = computerdb_load() if -e "$KLASK_DB_FILE";
   my $timestamp = time;

   my %computer_not_detected = ();
   my $timestamp_last_week = $timestamp - (3600 * 24 * 7);

   my $number_of_computer = get_list_ip(@network); # + 1;
   my $size_of_database   = keys %{$computerdb};
      $size_of_database   = 1 if $size_of_database == 0;
   my $i = 0;
   my $detected_computer = 0;

   init_switch_names('yes', $verb_description, $check_hostname, $check_location);    #nomme les switchs

   { # Remplis le champs portignore des ports d'inter-connection pour chaque switch
   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
   my %db_switch_output_port       = %{$switch_connection->{output_port}};
   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};
   my %db_switch_chained_port = ();
   for my $swport (keys %db_switch_connected_on_port) {
      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
      $db_switch_chained_port{$sw_connect} .= "$port_connect:";
      }
   for my $sw (@SWITCH){
      push @{$sw->{portignore}}, $db_switch_output_port{$sw->{hostname}}  if exists $db_switch_output_port{$sw->{hostname}};
      if ( exists $db_switch_chained_port{$sw->{hostname}} ) {
         chop $db_switch_chained_port{$sw->{hostname}};
         push @{$sw->{portignore}}, split m/ : /xms, $db_switch_chained_port{$sw->{hostname}};
         }
#      print "$sw->{hostname} ++ @{$sw->{portignore}}\n";
      }
   }

   my %router_mac_ip = ();
   DETECT_ALL_ROUTER:
#   for my $one_router ('194.254.66.254') {
   for my $one_router ( get_list_main_router(@network) ) {
      my %resol_arp = resolve_ip_arp_host($one_router);
      $router_mac_ip{ $resol_arp{mac_address} } = $resol_arp{ipv4_address};
      }

   ALL_NETWORK:
   for my $net (@network) {

      my @computer = get_list_ip($net);
      my $current_interface = get_current_interface($net);

      fast_ping(@computer);

      LOOP_ON_COMPUTER:
      for my $one_computer (@computer) {
         $i++;

         my $total_percent = int (($i*100)/$number_of_computer);

         my $localtime = time - $timestamp;
         my ($sec,$min) = localtime $localtime;

         my $time_elapse = 0;
            $time_elapse = $localtime * ( 100 - $total_percent) / $total_percent if $total_percent != 0;
         my ($sec_elapse,$min_elapse) = localtime $time_elapse;

         printf "\rComputer scanned: %4i/%i (%2i%%)",  $i,                 $number_of_computer, $total_percent;
         printf ', detected: %4i/%i (%2i%%)', $detected_computer, $size_of_database,   int(($detected_computer*100)/$size_of_database);
         printf ' [Time: %02i:%02i / %02i:%02i]', int($localtime/60), $localtime % 60, int($time_elapse/60), $time_elapse % 60;
         printf ' %-8s %-14s', $current_interface, $one_computer;

         my %resol_arp = resolve_ip_arp_host($one_computer, $current_interface);

         # do not search on router connection (why ?)
         if ( exists $router_mac_ip{$resol_arp{mac_address}}) {
            $computer_not_detected{$one_computer} = $current_interface;
            next LOOP_ON_COMPUTER;
            }

         # do not search on switch inter-connection
         if (exists $switch_level{$resol_arp{hostname_fq}}) {
            $computer_not_detected{$one_computer} = $current_interface;
            next LOOP_ON_COMPUTER;
            }

         my $switch_proposal = q{};
         if (exists $computerdb->{$resol_arp{ipv4_address}} and exists $computerdb->{$resol_arp{ipv4_address}}{switch_hostname}) {
            $switch_proposal = $computerdb->{$resol_arp{ipv4_address}}{switch_hostname};
            }

         # do not have a mac address
         if ($resol_arp{mac_address} eq 'unknow' or (exists $resol_arp{timestamps} and $resol_arp{timestamps} < ($timestamp - 3 * 3600))) {
            $computer_not_detected{$one_computer} = $current_interface;
            next LOOP_ON_COMPUTER;
            }

         my $vlan_name = get_current_vlan_name_for_interface($resol_arp{interface});
         my $vlan_id   = get_current_vlan_id($vlan_name);
         my %where = find_switch_port($resol_arp{mac_address},$switch_proposal,$vlan_id);

         #192.168.24.156:
         #  arp: 00:0B:DB:D5:F6:65
         #  hostname: pcroyon.hmg.priv
         #  port: 5
         #  switch: sw-batH-legi:hp2524
         #  timestamp: 1164355525

         # do not have a mac address
#         if ($resol_arp{mac_address} eq 'unknow') {
#            $computer_not_detected{$one_computer} = $current_interface;
#            next LOOP_ON_COMPUTER;
#            }

         # detected on a switch
         if ($where{switch_description} ne 'unknow') {
            $detected_computer++;
            $computerdb->{$resol_arp{ipv4_address}} = {
               hostname_fq        => $resol_arp{hostname_fq},
               mac_address        => $resol_arp{mac_address},
               switch_hostname    => $where{switch_hostname},
               switch_description => $where{switch_description},
               switch_port        => $where{switch_port},
               switch_port_hr     => $where{switch_port_hr},
               timestamp          => $timestamp,
               network            => $net,
               };
            next LOOP_ON_COMPUTER;
            }

         # new in the database but where it is ?
         if (not exists $computerdb->{$resol_arp{ipv4_address}}) {
            $detected_computer++;
            $computerdb->{$resol_arp{ipv4_address}} = {
               hostname_fq        => $resol_arp{hostname_fq},
               mac_address        => $resol_arp{mac_address},
               switch_hostname    => $where{switch_hostname},
               switch_description => $where{switch_description},
               switch_port        => $where{switch_port},
               switch_port_hr     => $where{switch_port_hr},
               timestamp          => $resol_arp{timestamp},
               network            => $net,
               };
            }

         # mise a jour du nom de la machine si modification dans le dns
         $computerdb->{$resol_arp{ipv4_address}}{hostname_fq} = $resol_arp{hostname_fq};

         # mise à jour de la date de détection si détection plus récente par arpwatch
         $computerdb->{$resol_arp{ipv4_address}}{timestamp}   = $resol_arp{timestamp} if exists $resol_arp{timestamp} and $computerdb->{$resol_arp{ipv4_address}}{timestamp} < $resol_arp{timestamp};

         # relance un arping sur la machine si celle-ci n'a pas été détectée depuis plus d'une semaine
#         push @computer_not_detected, $resol_arp{ipv4_address} if $computerdb->{$resol_arp{ipv4_address}}{timestamp} < $timestamp_last_week;
         $computer_not_detected{$resol_arp{ipv4_address}} = $current_interface if $computerdb->{$resol_arp{ipv4_address}}{timestamp} < $timestamp_last_week;

         }
      }

   # final end of line at the end of the loop
   printf "\n";

   my $dirdb = $KLASK_DB_FILE;
      $dirdb =~ s{ / [^/]* $}{}xms;
   mkdir "$dirdb", 0755 unless -d "$dirdb";
   YAML::Syck::DumpFile("$KLASK_DB_FILE", $computerdb);

   for my $one_computer (keys %computer_not_detected) {
      my $interface = $computer_not_detected{$one_computer};
      system "arping -c 1 -w 1 -rR -i $interface $one_computer &>/dev/null";
#      print  "arping -c 1 -w 1 -rR -i $interface $one_computer 2>/dev/null\n";
      }
   return;
   }

sub cmd_removedb {
   my @computer = @_;

   test_maindb_environnement();

   my $computerdb = computerdb_load();

   LOOP_ON_COMPUTER:
   for my $one_computer (@computer) {

      if ( $one_computer =~ m/^ $RE_IPv4_ADDRESS $/xms
            and exists $computerdb->{$one_computer} ) {
         delete $computerdb->{$one_computer};
         next;
         }

      my %resol_arp = resolve_ip_arp_host($one_computer);

      delete $computerdb->{$resol_arp{ipv4_address}} if exists $computerdb->{$resol_arp{ipv4_address}};
      }

   my $dirdb = $KLASK_DB_FILE;
      $dirdb =~ s{ / [^/]* $}{}xms;
   mkdir "$dirdb", 0755 unless -d "$dirdb";
   YAML::Syck::DumpFile("$KLASK_DB_FILE", $computerdb);
   return;
   }

sub cmd_cleandb {
   my @ARGV  = @_;

   my $days_to_clean = 15;
   my $verbose;
   my $database_has_changed;

   GetOptions(
      'day|d=i'   => \$days_to_clean,
      'verbose|v' => \$verbose,
      );

   my @vlan_name = get_list_network();

   my $computerdb = {};
      $computerdb = YAML::Syck::LoadFile("$KLASK_DB_FILE") if -e "$KLASK_DB_FILE";
   my $timestamp = time;

   my $timestamp_barrier = 3600 * 24 * $days_to_clean;
   my $timestamp_3month  = 3600 * 24 * 90;

   my %mactimedb = ();
   ALL_VLAN:
   for my $vlan (shuffle @vlan_name) {

      my @ip_list   = shuffle get_list_ip($vlan);
      
      LOOP_ON_IP_ADDRESS:
      for my $ip (@ip_list) {

         next LOOP_ON_IP_ADDRESS if
            not exists $computerdb->{$ip};
            
            #&& $computerdb->{$ip}{timestamp} > $timestamp_barrier;
         my $ip_timestamp   = $computerdb->{$ip}{timestamp};
         my $ip_mac         = $computerdb->{$ip}{mac_address};
         my $ip_hostname_fq = $computerdb->{$ip}{hostname_fq};

         $mactimedb{$ip_mac} ||= {
            ip          => $ip,
            timestamp   => $ip_timestamp,
            vlan        => $vlan,
            hostname_fq => $ip_hostname_fq,
            };
         
         if (
            ( $mactimedb{$ip_mac}->{timestamp} - $ip_timestamp > $timestamp_barrier
               or (
                  $mactimedb{$ip_mac}->{timestamp} > $ip_timestamp
                  and $timestamp - $mactimedb{$ip_mac}->{timestamp} > $timestamp_3month
                  )
            )
            and (
               not $mactimedb{$ip_mac}->{hostname_fq} =~ m/$RE_FLOAT_HOSTNAME/
               or $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/
               )) {
            print "remove ip $ip\n" if $verbose;
            delete $computerdb->{$ip};
            $database_has_changed++;
            }

         elsif (
            ( $ip_timestamp - $mactimedb{$ip_mac}->{timestamp} > $timestamp_barrier
               or (
                  $ip_timestamp > $mactimedb{$ip_mac}->{timestamp}
                  and $timestamp - $ip_timestamp > $timestamp_3month
                  )
            )
            and (
               not $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/
               or $mactimedb{$ip_mac}->{hostname_fq} =~ m/$RE_FLOAT_HOSTNAME/
               )) {
            print "remove ip ".$mactimedb{$ip_mac}->{ip}."\n" if $verbose;
            delete $computerdb->{$mactimedb{$ip_mac}->{ip}};
            $database_has_changed++;
            }

         if ( $ip_timestamp > $mactimedb{$ip_mac}->{timestamp}) {
            $mactimedb{$ip_mac} = {
               ip          => $ip,
               timestamp   => $ip_timestamp,
               vlan        => $vlan,
               hostname_fq => $ip_hostname_fq,
               };
            }
         }
      }

   if ( $database_has_changed ) {
      my $dirdb = $KLASK_DB_FILE;
         $dirdb =~ s{ / [^/]* $}{}xms;
      mkdir "$dirdb", 0755 unless -d "$dirdb";
      YAML::Syck::DumpFile("$KLASK_DB_FILE", $computerdb);
      }
   return;
   }

sub cmd_exportdb {
   @ARGV = @_;

   my $format = 'txt';

   GetOptions(
      'format|f=s'  => \$format,
      );

   my %possible_format = (
      txt  => \&cmd_exportdb_txt,
      html => \&cmd_exportdb_html,
      );

   $format = 'txt' if not defined $possible_format{$format};

   $possible_format{$format}->(@ARGV);
   return;
   }

sub cmd_exportdb_txt {
   test_maindb_environnement();

   my $computerdb = computerdb_load();

   printf "%-27s %-4s            %-40s %-15s %-18s %-16s %s\n", qw(Switch Port Hostname-FQ IPv4-Address MAC-Address Date VLAN);
   print "--------------------------------------------------------------------------------------------------------------------------------------------\n";

   LOOP_ON_IP_ADDRESS:
   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {

#      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq 'unknow';

      # to be improve in the future
      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !

# dans le futur
#      next if $computerdb->{$ip}{hostname_fq} eq 'unknow';

      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
      $year += 1900;
      $mon++;
      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;

      my $vlan = '';
      $vlan = $computerdb->{$ip}{network}.'('.get_current_vlan_id($computerdb->{$ip}{network}).')' if $computerdb->{$ip}{network};

      printf "%-28s  %2s  <-------  %-40s %-15s %-18s %-16s %s\n",
         $computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description},
         $computerdb->{$ip}{switch_port_hr},
         $computerdb->{$ip}{hostname_fq},
         $ip,
         $computerdb->{$ip}{mac_address},
         $date,
         $vlan;
      }
   return;
   }

sub cmd_exportdb_html {
   test_maindb_environnement();

   my $computerdb = computerdb_load();

#<link rel="stylesheet" type="text/css" href="style-klask.css" />
#<script src="sorttable-klask.js"></script>

   print <<'END_HTML';
<table class="sortable" summary="Klask Host Database">
 <caption>Klask Host Database</caption>
 <thead>
  <tr>
   <th scope="col" class="klask-header-left">Switch</th>
   <th scope="col" class="sorttable_nosort">Port</th>
   <th scope="col" class="sorttable_nosort" colspan="2">Link</th>
   <th scope="col" class="sorttable_alpha">Hostname-FQ</th>
   <th scope="col" class="hklask-ipv4">IPv4-Address</th>
   <th scope="col" class="sorttable_alpha">MAC-Address</th>
   <th scope="col" class="sorttable_alpha">VLAN</th>
   <th scope="col" class="klask-header-right">Date</th>
  </tr>
 </thead>
 <tfoot>
  <tr>
   <th scope="col" class="klask-footer-left">Switch</th>
   <th scope="col" class="fklask-port">Port</th>
   <th scope="col" class="fklask-link" colspan="2">Link</th>
   <th scope="col" class="fklask-hostname">Hostname-FQ</th>
   <th scope="col" class="fklask-ipv4">IPv4-Address</th>
   <th scope="col" class="fklask-mac">MAC-Address</th>
   <th scope="col" class="fklask-vlan">VLAN</th>
   <th scope="col" class="klask-footer-right">Date</th>
  </tr>
 </tfoot>
 <tbody>
END_HTML

   my %mac_count = ();
   LOOP_ON_IP_ADDRESS:
   for my $ip (keys %{$computerdb}) {

      # to be improve in the future
      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !

      $mac_count{$computerdb->{$ip}{mac_address}}++;
      }

   my $typerow = 'even';

   LOOP_ON_IP_ADDRESS:
   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {

      # to be improve in the future
      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !

      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
      $year += 1900;
      $mon++;
      my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;

#      $odd_or_even++;
#      my $typerow = $odd_or_even % 2 ? 'odd' : 'even';
      $typerow = $typerow eq 'even' ? 'odd' : 'even';

      my $arrow ='&#8592;';
         $arrow ='&#8656;' if $computerdb->{$ip}{switch_port_hr} =~ m/^(Trk|Br|Po)/;

      my $switch_hostname = $computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description} || 'unkown';
      chomp $switch_hostname;
      my $switch_hostname_sort = sprintf '%s %3s' ,$switch_hostname, $computerdb->{$ip}{switch_port_hr};

      my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ip;

      my $mac_sort = sprintf '%04i-%s', 9999 - $mac_count{$computerdb->{$ip}{mac_address}}, $computerdb->{$ip}{mac_address};

      $computerdb->{$ip}{hostname_fq} = 'unknow' if $computerdb->{$ip}{hostname_fq} =~ m/^ \d+ \. \d+ \. \d+ \. \d+ $/xms;
      my ( $host_short ) = split m/ \. /xms, $computerdb->{$ip}{hostname_fq};

      my $vlan = '';
      $vlan = $computerdb->{$ip}{network}.' ('.get_current_vlan_id($computerdb->{$ip}{network}).')' if $computerdb->{$ip}{network};

      print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$switch_hostname</td>
   <td class="bklask-port">$computerdb->{$ip}{switch_port_hr}</td>
   <td colspan="2">$arrow</td>
   <td sorttable_customkey="$host_short">$computerdb->{$ip}{hostname_fq}</td>
   <td sorttable_customkey="$ip_sort">$ip</td>
   <td sorttable_customkey="$mac_sort">$computerdb->{$ip}{mac_address}</td>
   <td>$vlan</td>
   <td>$date</td>
  </tr>
END_HTML
      }

   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");

   my %db_switch_output_port       = %{$switch_connection->{output_port}};
   my %db_switch_parent            = %{$switch_connection->{parent}};
   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};
   my %db_switch                   = %{$switch_connection->{switch_db}};

   for my $sw (sort keys %db_switch_output_port) {

      my $switch_hostname_sort = sprintf '%s %3s' ,$sw, $db_switch_output_port{$sw};

      $typerow = $typerow eq 'even' ? 'odd' : 'even';

      my $arrow ='&#8702;';
         $arrow ='&#8680;' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;

      if (exists $db_switch_parent{$sw}) {
         my $mac_address = $db_switch{$db_switch_parent{$sw}->{switch}}->{mac_address};
         my $ipv4_address = $db_switch{$db_switch_parent{$sw}->{switch}}->{ipv4_address};
         my $timestamp = $db_switch{$db_switch_parent{$sw}->{switch}}->{timestamp};

         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $timestamp;
         $year += 1900;
         $mon++;
         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;

         my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ipv4_address;

         my $mac_sort = sprintf '%04i-%s', 9999, $mac_address;

         my ( $host_short ) = sprintf '%s %3s' , split(m/ \. /xms, $db_switch_parent{$sw}->{switch}, 1), $db_switch_parent{$sw}->{port_hr};

         print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$sw</td>
   <td class="bklask-port">$db_switch_output_port{$sw}</td>
   <td>$arrow</td><td>$db_switch_parent{$sw}->{port_hr}</td>
   <td sorttable_customkey="$host_short">$db_switch_parent{$sw}->{switch}</td>
   <td sorttable_customkey="$ip_sort">$ipv4_address</td>
   <td sorttable_customkey="$mac_sort">$mac_address</td>
   <td></td>
   <td>$date</td>
  </tr>
END_HTML
         }
      else {
         print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$sw</td>
   <td class="bklask-port">$db_switch_output_port{$sw}</td>
   <td>$arrow</td><td></td>
   <td sorttable_customkey="router">router</td>
   <td sorttable_customkey="999999999999"></td>
   <td sorttable_customkey="99999"></td>
   <td></td>
   <td></td>
  </tr>
END_HTML
         }
      }

   for my $swport (sort keys %db_switch_connected_on_port) {
      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {

         my $switch_hostname_sort = sprintf '%s %3s' ,$sw_connect, $port_connect;

         my $mac_address = $db_switch{$sw}->{mac_address};
         my $ipv4_address = $db_switch{$sw}->{ipv4_address};
         my $timestamp = $db_switch{$sw}->{timestamp};

         my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $timestamp;
         $year += 1900;
         $mon++;
         my $date = sprintf '%04i-%02i-%02i %02i:%02i', $year,$mon,$mday,$hour,$min;

         my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ipv4_address;

         my $mac_sort = sprintf '%04i-%s', 9999, $mac_address;

         $typerow = $typerow eq 'even' ? 'odd' : 'even';

         my $arrow ='&#8701;';
            $arrow ='&#8678;' if $port_connect =~ m/^(Trk|Br|Po)/;

         if (exists $db_switch_output_port{$sw}) {

            my ( $host_short ) = sprintf '%s %3s' , split( m/\./xms, $sw, 1), $db_switch_output_port{$sw};

            print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
   <td class="bklask-port">$port_connect</td>
   <td>$arrow</td><td>$db_switch_output_port{$sw}</td>
   <td sorttable_customkey="$host_short">$sw</td>
   <td sorttable_customkey="$ip_sort">$ipv4_address</td>
   <td sorttable_customkey="$mac_sort">$mac_address</td>
   <td></td>
   <td>$date</td>
  </tr>
END_HTML
            }
         else {
            print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
   <td class="bklask-port">$port_connect</td>
   <td>$arrow</td><td></td>
   <td sorttable_customkey="$sw">$sw</td>
   <td sorttable_customkey="">$ipv4_address</td>
   <td sorttable_customkey="">$mac_address</td>
   <td></td>
   <td>$date</td>
  </tr>
END_HTML
            }
         }
      }

   print <<'END_HTML';
 </tbody>
</table>
END_HTML
   return;
   }

sub cmd_bad_vlan_id {
   test_maindb_environnement();

   my $computerdb = computerdb_load();

   # create a database with the most recent computer by switch port
   my %swithportdb = ();
   LOOP_ON_IP_ADDRESS:
   for my $ip (keys %{$computerdb}) {
      # to be improve in the future
      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !
      next LOOP_ON_IP_ADDRESS if ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}) eq 'unknow';
      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{switch_port} eq '0';

      my $ip_timestamp   = $computerdb->{$ip}{timestamp};
      my $ip_mac         = $computerdb->{$ip}{mac_address};
      my $ip_hostname_fq = $computerdb->{$ip}{hostname_fq};

      my $swpt = sprintf "%-28s  %2s",
         $computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description},
         $computerdb->{$ip}{switch_port_hr};
      $swithportdb{$swpt} ||= {
         ip          => $ip,
         timestamp   => $ip_timestamp,
         vlan        => $computerdb->{$ip}{network},
         hostname_fq => $ip_hostname_fq,
         mac_address => $ip_mac,
         };

      # if float computer, set date 15 day before warning...
      my $ip_timestamp_mod = $ip_timestamp;
      my $ip_timestamp_ref = $swithportdb{$swpt}->{timestamp};
      $ip_timestamp_mod -= 15 * 24 * 3600 if $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;
      $ip_timestamp_ref -= 15 * 24 * 3600 if $swithportdb{$swpt}->{hostname_fq} =~ m/$RE_FLOAT_HOSTNAME/;
      
      if ($ip_timestamp_mod > $ip_timestamp_ref) {
         $swithportdb{$swpt} = {
            ip          => $ip,
            timestamp   => $ip_timestamp,
            vlan        => $computerdb->{$ip}{network},
            hostname_fq => $ip_hostname_fq,
            mac_address => $ip_mac,
            };
         }
      }

   LOOP_ON_RECENT_COMPUTER:
   for my $swpt (keys %swithportdb) {
      next LOOP_ON_RECENT_COMPUTER if $swpt =~ m/^\s*0$/;
      next LOOP_ON_RECENT_COMPUTER if $swithportdb{$swpt}->{hostname_fq} !~ m/$RE_FLOAT_HOSTNAME/;

      my $src_ip = $swithportdb{$swpt}->{ip};
      my $src_timestamp = 0;
      LOOP_ON_IP_ADDRESS:
      for my $ip (keys %{$computerdb}) {
         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{mac_address} ne  $swithportdb{$swpt}->{mac_address};
         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} =~ m/$RE_FLOAT_HOSTNAME/;
         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{timestamp} < $src_timestamp;
         
         $src_ip = $ip;
         $src_timestamp = $computerdb->{$ip}{timestamp};
         }

      # keep only if float computer is the most recent
      next LOOP_ON_RECENT_COMPUTER if $src_timestamp == 0;
      next LOOP_ON_RECENT_COMPUTER if $swithportdb{$swpt}->{timestamp} < $src_timestamp;

      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $swithportdb{$swpt}->{timestamp};
      $year += 1900;
      $mon++;
      my $date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;

      ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$src_ip}{timestamp};
      $year += 1900;
      $mon++;
      my $src_date = sprintf '%04i-%02i-%02i/%02i:%02i', $year, $mon, $mday, $hour, $min;

      my $vlan_id = get_current_vlan_id($computerdb->{$src_ip}{network});

      printf "%s / %-10s +-> %-10s(%i)  %s %s %s %s\n",
         $swpt, $swithportdb{$swpt}->{vlan}, $computerdb->{$src_ip}{network}, $vlan_id,
         $date,
         $src_date,
         $computerdb->{$src_ip}{mac_address},
         $computerdb->{$src_ip}{hostname_fq};
      }
   }

sub cmd_set_vlan_port {
   my $switch_name = shift || q{};
   my $mac_address = shift || q{};

   if ($switch_name eq q{} or $mac_address eq q{}) {
      die "Usage: klask search-mac-on-switch SWITCH_NAME MAC_ADDRESS\n";
      }

   $switch_name = join(',', map {$_->{hostname}} @SWITCH ) if $switch_name eq q{*};

   for my $sw_name (split /,/, $switch_name) {
      if (not defined $SWITCH_DB{$sw_name}) {
         die "Switch $sw_name must be defined in klask configuration file\n";
         }

      my $sw = $SWITCH_DB{$sw_name};
      my %session = ( -hostname => $sw->{hostname} );
         $session{-version} = $sw->{version}   || 1;
         $session{-port}    = $sw->{snmpport}  || $DEFAULT{snmpport}  || 161;
      if (exists $sw->{version} and $sw->{version} eq '3') {
         $session{-username} = $sw->{username} || 'snmpadmin';
         }
      else {
         $session{-community} = $sw->{community} || $DEFAULT{community} || 'public';
         }

      my $research1 = $OID_NUMBER{searchPort1} . mac_address_hex_to_dec($mac_address);
      my $research2 = $OID_NUMBER{searchPort2} .'.'. 0 . mac_address_hex_to_dec($mac_address);
      print "Klask search OID $research1 on switch $sw_name\n";
      print "Klask search OID $research2 on switch $sw_name\n";

      my ($session, $error) = Net::SNMP->session( %session );
      print "$error \n" if $error;

      my $result = $session->get_request(
         -varbindlist => [$research1]
         );
      if (not defined $result) {
         $result = $session->get_request(
            -varbindlist => [$research2]
            );
         $result->{$research1} = $result->{$research2} if defined $result;
         }

      if (defined $result and $result->{$research1} ne 'noSuchInstance') {
         my $swport = $result->{$research1};
         print "Klask find MAC $mac_address on switch $sw_name port $swport\n";
         }
      else {
         print "Klask do not find MAC $mac_address on switch $sw_name\n";
         }

      $session->close;
      }
   return;
   }

sub cmd_get_vlan_port {
   @ARGV = @_;

   my $verbose;
   GetOptions(
      'verbose|v' => \$verbose,
      );

   my $switch_name = shift @ARGV || q{};
   my $switch_port = shift @ARGV || q{};

   if ($switch_name eq q{} or $switch_port eq q{}) {
      die "Usage: klask get-vlan-port SWITCH_NAME PORT\n";
      }

   for my $sw_name (split /,/, $switch_name) {
      if (not defined $SWITCH_DB{$sw_name}) {
         die "Switch $sw_name must be defined in klask configuration file\n";
         }

      my $sw = $SWITCH_DB{$sw_name};
      my %session = ( -hostname => $sw->{hostname} );
         $session{-version} = $sw->{version}   || 1;
         $session{-port}    = $sw->{snmpport}  || $DEFAULT{snmpport}  || 161;
      if (exists $sw->{version} and $sw->{version} eq '3') {
         $session{-username} = $sw->{username} || 'snmpadmin';
         }
      else {
         $session{-community} = $sw->{community} || $DEFAULT{community} || 'public';
         }

      my $search = $OID_NUMBER{'vlanPortDefault'} . ".$switch_port";

      my ($session, $error) = Net::SNMP->session( %session );
      print "$error \n" if $error;

      my $result = $session->get_request(
         -varbindlist => [$search],
         );

      if (defined $result and $result->{$search} ne 'noSuchInstance') {
         my $vlan_id = $result->{$search} || 'empty';
         print "Klask VLAN Id $vlan_id on switch $sw_name on port $switch_port\n";
         }
      else {
         print "Klask do not find VLAN Id on switch $sw_name on port $switch_port\n";
         }

      $session->close;
      }
   return;
   }

sub cmd_set_vlan_name {
   }

# snmpset -v 1 -c public sw1-batG0-legi.hmg.priv "$OID_NUMBER{'hpicfReset'}.0" i 2;
sub cmd_rebootsw {
   @ARGV = @_;

   my $verbose;
   GetOptions(
      'verbose|v' => \$verbose,
      );

   my $switch_name = shift @ARGV || q{};

   if ($switch_name eq q{}) {
      die "Usage: klask rebootsw SWITCH_NAME\n";
      }

   for my $sw_name (split /,/, $switch_name) {
      if (not defined $SWITCH_DB{$sw_name}) {
         die "Switch $sw_name must be defined in klask configuration file\n";
         }

      my $sw = $SWITCH_DB{$sw_name};
      my %session = ( -hostname => $sw->{hostname} );
         $session{-version} = $sw->{version}   || 1;
         $session{-port}    = $sw->{snmpport}  || $DEFAULT{snmpport}  || 161;
      if (exists $sw->{version} and $sw->{version} eq '3') {
         $session{-username} = $sw->{username} || 'snmpadmin';
         }
      else {
         $session{-community} = $sw->{community} || $DEFAULT{community} || 'public';
         }

      my ($session, $error) = Net::SNMP->session( %session );
      print "$error \n" if $error;

      my $result = $session->set_request(
         -varbindlist => ["$OID_NUMBER{'hpicfReset'}.0", INTEGER, 2],
         );

      $session->close;
      }
   return;
   }

sub cmd_get_vlan_name {
   my $switch_name = shift || q{};
   my $vlan_id     = shift || q{};

   if ($switch_name eq q{} or $vlan_id eq q{}) {
      die "Usage: klask get-vlan-name SWITCH_NAME VLAN_ID\n";
      }

   $switch_name = join(',', map {$_->{hostname}} @SWITCH ) if $switch_name eq q{*};

   for my $sw_name (split /,/, $switch_name) {
      if (not defined $SWITCH_DB{$sw_name}) {
         die "Switch $sw_name must be defined in klask configuration file\n";
         }

      my $sw = $SWITCH_DB{$sw_name};
      my %session = ( -hostname => $sw->{hostname} );
         $session{-version} = $sw->{version}   || 1;
         $session{-port}    = $sw->{snmpport}  || $DEFAULT{snmpport}  || 161;
      if (exists $sw->{version} and $sw->{version} eq '3') {
         $session{-username} = $sw->{username} || 'snmpadmin';
         }
      else {
         $session{-community} = $sw->{community} || $DEFAULT{community} || 'public';
         }

      my $search_vlan_name = $OID_NUMBER{vlanName} . ".$vlan_id";

      my ($session, $error) = Net::SNMP->session( %session );
      print "$error \n" if $error;

      my $result = $session->get_request(
         -varbindlist => [$search_vlan_name]
         );

      if (defined $result and $result->{$search_vlan_name} ne 'noSuchInstance') {
         my $vlan_name = $result->{$search_vlan_name} || 'empty';
         print "Klask find VLAN $vlan_id on switch $sw_name with name $vlan_name\n";
         }
      else {
         print "Klask do not find VLAN $vlan_id on switch $sw_name\n";
         }

      $session->close;
      }
   return;
   }

sub cmd_ip_location {
   my $computerdb = computerdb_load();

   LOOP_ON_IP_ADDRESS:
   for my $ip (Net::Netmask::sort_by_ip_address(keys %{$computerdb})) {

      next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{hostname_fq} eq ($computerdb->{$ip}{switch_hostname} || $computerdb->{$ip}{switch_description}); # switch on himself !

      my $sw_hostname = $computerdb->{$ip}{switch_hostname} || q{};
      next LOOP_ON_IP_ADDRESS if $sw_hostname eq 'unknow';

      my $sw_location = q{};
      LOOP_ON_ALL_SWITCH:
      for my $sw (@SWITCH) {
         next LOOP_ON_ALL_SWITCH if $sw_hostname ne $sw->{hostname};
         $sw_location = $sw->{location};
         last;
         }

      printf "%s: \"%s\"\n", $ip, $sw_location if not $sw_location eq q{};
      }
   return;
   }

sub cmd_ip_free {
   @ARGV = @_; # VLAN name with option

   my $days_to_dead = 365 * 2;
   my $format = 'txt';
   my $verbose;

   GetOptions(
      'day|d=i'      => \$days_to_dead,
      'format|f=s'   => \$format,
      'verbose|v'    => \$verbose,
      );

   my %possible_format = (
      txt  => \&cmd_ip_free_txt,
      html => \&cmd_ip_free_html,
      none => sub {},
      );
   $format = 'txt' if not defined $possible_format{$format};

   my @vlan_name = @ARGV;
   @vlan_name = get_list_network() if not @vlan_name;

   my $computerdb = {};
      $computerdb = computerdb_load() if -e "$KLASK_DB_FILE";
   my $timestamp = time;

   my $timestamp_barrier = $timestamp - (3600 * 24 * $days_to_dead );

   my %result_ip = ();

   ALL_NETWORK:
   for my $vlan (@vlan_name) {

      my @ip_list = get_list_ip($vlan);

      LOOP_ON_IP_ADDRESS:
      for my $ip (@ip_list) {

         if (exists $computerdb->{$ip}) {
            next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{timestamp} > $timestamp_barrier;
            
            my $mac_address = $computerdb->{$ip}{mac_address};
            LOOP_ON_DATABASE:
            for my $ip_db (keys %{$computerdb}) {
               next LOOP_ON_DATABASE if $computerdb->{$ip_db}{mac_address} ne $mac_address;
               next LOOP_ON_IP_ADDRESS if $computerdb->{$ip_db}{timestamp} > $timestamp_barrier;
               }
            }

         my $ip_date_last_detection = '';
         if (exists $computerdb->{$ip}) {
            my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $computerdb->{$ip}{timestamp};
            $year += 1900;
            $mon++;
            $ip_date_last_detection = sprintf '%04i-%02i-%02i %02i:%02i', $year, $mon, $mday, $hour, $min;
            }

         my $packed_ip = scalar gethostbyname($ip);
         my $hostname_fq = 'unknown';
            $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET) || 'unknown' if defined $packed_ip;

         next LOOP_ON_IP_ADDRESS if $hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;

         $result_ip{$ip} ||= {};
         $result_ip{$ip}->{date_last_detection} = $ip_date_last_detection;
         $result_ip{$ip}->{hostname_fq} = $hostname_fq;
         $result_ip{$ip}->{vlan} = $vlan;

         printf "VERBOSE_1: %-15s %-12s %s\n", $ip, $vlan, $hostname_fq if $verbose;
         }
      }

   $possible_format{$format}->(%result_ip);
   }

sub cmd_ip_free_txt {
   my %result_ip = @_;
   
   printf "%-15s %-40s %-16s %s\n", qw(IPv4-Address Hostname-FQ Date VLAN);
   print "-------------------------------------------------------------------------------\n";
   LOOP_ON_IP_ADDRESS:
   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {
         printf "%-15s %-40s %-16s %s\n", $ip, $result_ip{$ip}->{hostname_fq}, $result_ip{$ip}->{date_last_detection}, $result_ip{$ip}->{vlan};
      }
   }

sub cmd_ip_free_html {
   my %result_ip = @_;

   print <<'END_HTML';
<table class="sortable" summary="Klask Free IP Database">
 <caption>Klask Free IP Database</caption>
 <thead>
  <tr>
   <th scope="col" class="klask-header-left">IPv4-Address</th>
   <th scope="col" class="sorttable_alpha">Hostname-FQ</th>
   <th scope="col" class="sorttable_alpha">VLAN</th>
   <th scope="col" class="klask-header-right">Date</th>
  </tr>
 </thead>
 <tfoot>
  <tr>
   <th scope="col" class="klask-footer-left">IPv4-Address</th>
   <th scope="col" class="fklask-hostname">Hostname-FQ</th>
   <th scope="col" class="fklask-vlan">VLAN</th>
   <th scope="col" class="klask-footer-right">Date</th>
  </tr>
 </tfoot>
 <tbody>
END_HTML

   my $typerow = 'even';

   LOOP_ON_IP_ADDRESS:
   for my $ip (Net::Netmask::sort_by_ip_address(keys %result_ip)) {

      $typerow = $typerow eq 'even' ? 'odd' : 'even';

      my $ip_sort = sprintf '%03i%03i%03i%03i', split m/ \. /xms, $ip;
      my ( $host_short ) = split m/ \. /xms, $result_ip{$ip}->{hostname_fq};

      print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$ip_sort">$ip</td>
   <td sorttable_customkey="$host_short">$result_ip{$ip}->{hostname_fq}</td>
   <td>$result_ip{$ip}->{vlan}</td>
   <td>$result_ip{$ip}->{date_last_detection}</td>
  </tr>
END_HTML
      }
   print <<'END_HTML';
 </tbody>
</table>
END_HTML
   }

sub cmd_enable {
   my $switch = shift;
   my $port   = shift;

   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 1 (up)
   #snmpset -v 1 -c community X.X.X.X 1.3.6.1.2.1.2.2.1.7.NoPort = 2 (down)
   system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 1";
   return;
   }

sub cmd_disable {
   my $switch = shift;
   my $port   = shift;

   system "snmpset -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port = 2";
   return;
   }

sub cmd_status {
   my $switch = shift;
   my $port   = shift;

   system "snmpget -v 1 -c public $switch 1.3.6.1.2.1.2.2.1.7.$port";
   return;
   }

sub cmd_search_mac_on_switch {
   @ARGV = @_;

   my $verbose;
   my $vlan_id = 0;

   GetOptions(
      'verbose|v' => \$verbose,
      'vlan|l=i'  => \$vlan_id,
      );

   my $switch_name = shift @ARGV || q{};
   my $mac_address = shift @ARGV || q{};

   if ($switch_name eq q{} or $mac_address eq q{}) {
      die "Usage: klask search-mac-on-switch SWITCH_NAME MAC_ADDRESS\n";
      }

   $mac_address = normalize_mac_address($mac_address);
   $switch_name = join(',', map {$_->{hostname}} @SWITCH ) if $switch_name eq q{*};

   for my $sw_name (split /,/, $switch_name) {
      if (not defined $SWITCH_DB{$sw_name}) {
         die "Switch $sw_name must be defined in klask configuration file\n";
         }

      my $sw = $SWITCH_DB{$sw_name};
      my %session = ( -hostname => $sw->{hostname} );
         $session{-version} = $sw->{version}   || 1;
         $session{-port}    = $sw->{snmpport}  || $DEFAULT{snmpport}  || 161;
      if (exists $sw->{version} and $sw->{version} eq '3') {
         $session{-username} = $sw->{username} || 'snmpadmin';
         }
      else {
         $session{-community} = $sw->{community} || $DEFAULT{community} || 'public';
         }

      my $research1 = $OID_NUMBER{searchPort1} . mac_address_hex_to_dec($mac_address);
      my $research2 = $OID_NUMBER{searchPort2} .'.'. $vlan_id . mac_address_hex_to_dec($mac_address);
      print "Klask search OID $research1 on switch $sw_name\n" if $verbose;
      print "Klask search OID $research2 on switch $sw_name\n" if $verbose;

      my ($session, $error) = Net::SNMP->session( %session );
      print "$error \n" if $error;

      my $result = $session->get_request(
         -varbindlist => [$research1]
         );
      if (not defined $result) {
         $result = $session->get_request(
            -varbindlist => [$research2]
            );
         $result->{$research1} = $result->{$research2} if defined $result;
         }

      if (defined $result and $result->{$research1} ne 'noSuchInstance') {
         my $swport = $result->{$research1};
         print "Klask find MAC $mac_address on switch $sw_name port $swport\n";
         }
      else {
         print "Klask do not find MAC $mac_address on switch $sw_name\n" if $verbose;
         }

      $session->close;
      }
   return;
   }

sub cmd_updatesw {
   @ARGV = @_;

   my $verbose;

   GetOptions(
      'verbose|v' => \$verbose,
      );

   init_switch_names('yes');    #nomme les switchs
   print "\n";

   my %where = ();
   my %db_switch_output_port = ();
   my %db_switch_ip_hostnamefq = ();

   DETECT_ALL_ROUTER:
#   for my $one_computer ('194.254.66.254') {
   for my $one_router ( get_list_main_router(get_list_network()) ) {
      my %resol_arp = resolve_ip_arp_host($one_router, q{*}, q{low}); # resolution arp

      next DETECT_ALL_ROUTER if $resol_arp{mac_address} eq 'unknow';
      print "VERBOSE_1: Router detected $resol_arp{ipv4_address} - $resol_arp{mac_address}\n" if $verbose;

      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{interface});
      my $vlan_id   = get_current_vlan_id($vlan_name);
      $where{$resol_arp{ipv4_address}} = find_all_switch_port($resol_arp{mac_address},$vlan_id); # retrouve les emplacements des routeurs
      }

   ALL_ROUTER_IP_ADDRESS:
   for my $ip_router (Net::Netmask::sort_by_ip_address(keys %where)) { # '194.254.66.254')) {

      next ALL_ROUTER_IP_ADDRESS if not exists $where{$ip_router}; # /a priori/ idiot car ne sers à rien...

      ALL_SWITCH_CONNECTED:
      for my $switch_detected ( keys %{$where{$ip_router}} ) {

         my $switch = $where{$ip_router}->{$switch_detected};

         next ALL_SWITCH_CONNECTED if $switch->{port} eq '0';

         $db_switch_output_port{$switch->{hostname}} = $switch->{port_hr};
         print "VERBOSE_2: output port $switch->{hostname} : $switch->{port_hr}\n" if $verbose;
         }
      }

   my %db_switch_link_with = ();

   my @list_all_switch = ();
   my @list_switch_ipv4 = ();
   for my $sw (@SWITCH){
      push @list_all_switch, $sw->{hostname};
      }

   my $timestamp = time;

   ALL_SWITCH:
   for my $one_computer (@list_all_switch) {
      my %resol_arp = resolve_ip_arp_host($one_computer, q{*}, q{low}); # arp resolution
      next ALL_SWITCH if $resol_arp{mac_address} eq 'unknow';

      push @list_switch_ipv4, $resol_arp{ipv4_address};

      my $vlan_name = get_current_vlan_name_for_interface($resol_arp{interface});
      my $vlan_id   = get_current_vlan_id($vlan_name);
      $where{$resol_arp{ipv4_address}} = find_all_switch_port($resol_arp{mac_address},$vlan_id); # find port on all switch

      if ($verbose) {
         print "VERBOSE_3: $one_computer $resol_arp{ipv4_address} $resol_arp{mac_address}\n";
         print "VERBOSE_3: $one_computer --- ",
            join(' + ', keys %{$where{$resol_arp{ipv4_address}}}),
            "\n";
         }

      $db_switch_ip_hostnamefq{$resol_arp{ipv4_address}} = $resol_arp{hostname_fq};
      print "VERBOSE_4: db_switch_ip_hostnamefq $resol_arp{ipv4_address} -> $resol_arp{hostname_fq}\n" if $verbose;

      $SWITCH_DB{$one_computer}->{ipv4_address} = $resol_arp{ipv4_address};
      $SWITCH_DB{$one_computer}->{mac_address}  = $resol_arp{mac_address};
      $SWITCH_DB{$one_computer}->{timestamp}    = $timestamp;
      }

   ALL_SWITCH_IP_ADDRESS:
   for my $ip (Net::Netmask::sort_by_ip_address(@list_switch_ipv4)) {

      print "VERBOSE_5: loop on $db_switch_ip_hostnamefq{$ip}\n" if $verbose;

      next ALL_SWITCH_IP_ADDRESS if not exists $where{$ip};
#      next ALL_SWITCH_IP_ADDRESS if not exists $SWITCH_PORT_COUNT{ $db_switch_ip_hostnamefq{$ip} };

      DETECTED_SWITCH:
      for my $switch_detected ( keys %{$where{$ip}} ) {

         my $switch = $where{$ip}->{$switch_detected};
         print "VERBOSE_6: $db_switch_ip_hostnamefq{$ip} -> $switch->{hostname} : $switch->{port_hr}\n" if $verbose;

         next if $switch->{port}     eq '0';
         next if $switch->{port_hr}  eq $db_switch_output_port{$switch->{hostname}};
         next if $switch->{hostname} eq $db_switch_ip_hostnamefq{$ip}; # $computerdb->{$ip}{hostname};

         $db_switch_link_with{ $db_switch_ip_hostnamefq{$ip} } ||= {};
         $db_switch_link_with{ $db_switch_ip_hostnamefq{$ip} }->{ $switch->{hostname} } = $switch->{port_hr};
         print "VERBOSE_7: +++++\n" if $verbose;
         }

      }

   my %db_switch_connected_on_port = ();
   my $maybe_more_than_one_switch_connected = 'yes';

   while ($maybe_more_than_one_switch_connected eq 'yes') {
      for my $sw (keys %db_switch_link_with) {
         for my $connect (keys %{$db_switch_link_with{$sw}}) {

            my $port_hr = $db_switch_link_with{$sw}->{$connect};

            $db_switch_connected_on_port{"$connect:$port_hr"} ||= {};
            $db_switch_connected_on_port{"$connect:$port_hr"}->{$sw}++; # Just to define the key
            }
         }

      $maybe_more_than_one_switch_connected  = 'no';

      SWITCH_AND_PORT:
      for my $swport (keys %db_switch_connected_on_port) {

         next if keys %{$db_switch_connected_on_port{$swport}} == 1;

         $maybe_more_than_one_switch_connected = 'yes';

         my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
         my @sw_on_same_port = keys %{$db_switch_connected_on_port{$swport}};

         CONNECTED:
         for my $sw_connected (@sw_on_same_port) {

            next CONNECTED if not keys %{$db_switch_link_with{$sw_connected}} == 1;

            $db_switch_connected_on_port{$swport} = {$sw_connected => 1};

            for my $other_sw (@sw_on_same_port) {
               next if $other_sw eq $sw_connected;

               delete $db_switch_link_with{$other_sw}->{$sw_connect};
               }

            # We can not do better for this switch for this loop
            next SWITCH_AND_PORT;
            }
         }
      }

   my %db_switch_parent =();

   for my $sw (keys %db_switch_link_with) {
      for my $connect (keys %{$db_switch_link_with{$sw}}) {

         my $port_hr = $db_switch_link_with{$sw}->{$connect};

         $db_switch_connected_on_port{"$connect:$port_hr"} ||= {};
         $db_switch_connected_on_port{"$connect:$port_hr"}->{$sw} = $port_hr;

         $db_switch_parent{$sw} = {switch => $connect, port_hr => $port_hr};
         }
      }

   print "Switch output port and parent port connection\n";
   print "---------------------------------------------\n";
   for my $sw (sort keys %db_switch_output_port) {
      if (exists $db_switch_parent{$sw}) {
         printf "%-28s  %2s  +-->  %2s  %-25s\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{port_hr}, $db_switch_parent{$sw}->{switch};
         }
      else {
         printf "%-28s  %2s  +-->  router\n", $sw, $db_switch_output_port{$sw};
         }
      }
   print "\n";

   print "Switch parent and children port inter-connection\n";
   print "------------------------------------------------\n";
   for my $swport (sort keys %db_switch_connected_on_port) {
      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
         if (exists $db_switch_output_port{$sw}) {
            printf "%-28s  %2s  <--+  %2s  %-25s\n", $sw_connect, $port_connect, $db_switch_output_port{$sw}, $sw;
            }
         else {
            printf "%-28s  %2s  <--+      %-25s\n", $sw_connect, $port_connect, $sw;
            }
         }
      }

   my $switch_connection = {
      output_port       => \%db_switch_output_port,
      parent            => \%db_switch_parent,
      connected_on_port => \%db_switch_connected_on_port,
      link_with         => \%db_switch_link_with,
      switch_db         => \%SWITCH_DB,
      };

   YAML::Syck::DumpFile("$KLASK_SW_FILE", $switch_connection);
   return;
   }

sub cmd_exportsw {
   @ARGV = @_;

   test_switchdb_environnement();

   my $format = 'txt';

   GetOptions(
      'format|f=s'  => \$format,
      );

   my %possible_format = (
      txt => \&cmd_exportsw_txt,
      dot => \&cmd_exportsw_dot,
      );

   $format = 'txt' if not defined $possible_format{$format};

   $possible_format{$format}->(@ARGV);
   return;
   }

sub cmd_exportsw_txt {

   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");

   my %db_switch_output_port       = %{$switch_connection->{output_port}};
   my %db_switch_parent            = %{$switch_connection->{parent}};
   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};

   print "Switch output port and parent port connection\n";
   print "---------------------------------------------\n";
   for my $sw (sort keys %db_switch_output_port) {
      if (exists $db_switch_parent{$sw}) {
         printf "%-28s  %2s  +-->  %2s  %-25s\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{port_hr}, $db_switch_parent{$sw}->{switch};
         }
      else {
         printf "%-28s  %2s  +-->  router\n", $sw, $db_switch_output_port{$sw};
         }
      }
   print "\n";

   print "Switch parent and children port inter-connection\n";
   print "------------------------------------------------\n";
   for my $swport (sort keys %db_switch_connected_on_port) {
      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
         if (exists $db_switch_output_port{$sw}) {
            printf "%-28s  %2s  <--+  %2s  %-25s\n", $sw_connect, $port_connect, $db_switch_output_port{$sw}, $sw;
            }
         else {
            printf "%-28s  %2s  <--+      %-25s\n", $sw_connect, $port_connect, $sw;
            }
         }
      }
   return;
   }

sub cmd_exportsw_dot {

   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");

   my %db_switch_output_port       = %{$switch_connection->{output_port}};
   my %db_switch_parent            = %{$switch_connection->{parent}};
   my %db_switch_connected_on_port = %{$switch_connection->{connected_on_port}};
   my %db_switch_link_with         = %{$switch_connection->{link_with}};
   my %db_switch_global            = %{$switch_connection->{switch_db}};

   my %db_building= ();
   for my $sw (@SWITCH) {
      my ($building, $location) = split m/ \/ /xms, $sw->{location}, 2;
      $db_building{$building} ||= {};
      $db_building{$building}->{$location} ||= {};
      $db_building{$building}->{$location}{ $sw->{hostname} } = 'y';
      }


   print "digraph G {\n";
   print "rankdir = LR;\n";

   print "site [label = \"site\", color = black, fillcolor = gold, shape = invhouse, style = filled];\n";
   print "internet [label = \"internet\", color = black, fillcolor = cyan, shape = house, style = filled];\n";

   my $b=0;
   for my $building (keys %db_building) {
      $b++;

      print "\"building$b\" [label = \"$building\", color = black, fillcolor = gold, style = filled];\n";
      print "site -> \"building$b\" [len = 2, color = firebrick];\n";

      my $l = 0;
      for my $loc (keys %{$db_building{$building}}) {
         $l++;

         print "\"location$b-$l\" [label = \"$building" . q{/} . join(q{\n}, split(m{ / }xms, $loc)) . "\", color = black, fillcolor = orange, style = filled];\n";
#         print "\"location$b-$l\" [label = \"$building / $loc\", color = black, fillcolor = orange, style = filled];\n";
         print "\"building$b\" -> \"location$b-$l\" [len = 2, color = firebrick]\n";

         for my $sw (keys %{$db_building{$building}->{$loc}}) {

            print "\"$sw:$db_switch_output_port{$sw}\" [label = \"$db_switch_output_port{$sw}\", color = black, fillcolor = lightblue,  peripheries = 2, style = filled];\n";

            my $swname  = $sw;
               $swname .= q{\n-\n} . "$db_switch_global{$sw}->{model}" if exists $db_switch_global{$sw} and exists $db_switch_global{$sw}->{model};
            print "\"$sw\" [label = \"$swname\", color = black, fillcolor = palegreen, shape = rect, style = filled];\n";
            print "\"location$b-$l\" -> \"$sw\" [len = 2, color = firebrick, arrowtail = dot]\n";
            print "\"$sw\" -> \"$sw:$db_switch_output_port{$sw}\" [len=2, style=bold, arrowhead = normal, arrowtail = invdot]\n";


            for my $swport (keys %db_switch_connected_on_port) {
               my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
               next if not $sw_connect eq $sw;
               next if $port_connect eq $db_switch_output_port{$sw};
               print "\"$sw:$port_connect\" [label = \"$port_connect\", color = black, fillcolor = plum,  peripheries = 1, style = filled];\n";
               print "\"$sw:$port_connect\" -> \"$sw\" [len=2, style=bold, arrowhead= normal, arrowtail = inv]\n";
              }
            }
         }
      }

#   print "Switch output port and parent port connection\n";
#   print "---------------------------------------------\n";
   for my $sw (sort keys %db_switch_output_port) {
      if (exists $db_switch_parent{$sw}) {
#         printf "   \"%s:%s\" -> \"%s:%s\"\n", $sw, $db_switch_output_port{$sw}, $db_switch_parent{$sw}->{switch}, $db_switch_parent{$sw}->{port};
         }
      else {
         printf "   \"%s:%s\" -> internet\n", $sw, $db_switch_output_port{$sw};
         }
      }
   print "\n";

#   print "Switch parent and children port inter-connection\n";
#   print "------------------------------------------------\n";
   for my $swport (sort keys %db_switch_connected_on_port) {
      my ($sw_connect,$port_connect) = split m/ : /xms, $swport;
      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
         if (exists $db_switch_output_port{$sw}) {
            printf "   \"%s:%s\" -> \"%s:%s\" [color = navyblue]\n", $sw, $db_switch_output_port{$sw}, $sw_connect, $port_connect;
            }
         else {
            printf "   \"%s\"   -> \"%s%s\"\n", $sw, $sw_connect, $port_connect;
            }
         }
      }

print "}\n";
   return;
   }


__END__

=head1 NAME

klask - ports manager and finder for switch


=head1 USAGE

 klask updatedb
 klask exportdb --format [txt|html]
 klask removedb computer*
 klask cleandb  --day number_of_day --verbose

 klask updatesw
 klask exportsw --format [txt|dot]

 klask searchdb --kind [host|mac] computer [mac-address]
 klask search   computer
 klask search-mac-on-switch switch mac_addr

 klask ip-free --day number_of_day --format [txt|html] [vlan_name]

 klask enable  switch port
 klask disable swith port
 klask status  swith port


=head1 DESCRIPTION

klask is a small tool to find where is a host in a big network. klask mean search in brittany.

Klask has now a web site dedicated for it !

 http://servforge.legi.grenoble-inp.fr/projects/klask


=head1 COMMANDS


=head2 search

This command takes one or more computer in argument. It search a computer on the network and give the port and the switch on which the computer is connected.


=head2 enable

This command activate a port on a switch by snmp. So you need to give the switch and the port number on the command line.


=head2 disable

This command deactivate a port on a switch by snmp. So you need to give the switch and the port number on the command line.


=head2 status

This command return the status of a port number on a switch by snmp. So you need to give the switch name and the port number on the command line.


=head2 updatedb

This command will scan networks and update a database. To know which are the cmputer scan, you have to configure the file /etc/klask/klask.conf This file is easy to read and write because klask use YAML format and not XML.


=head2 exportdb

This command print the content of the database. There is actually only one format. It's very easy to have more format, it's just need times...


=head2 updatesw

This command build a map of your manageable switch on your network. The list of the switch must be given in the file /etc/klask/klask.conf.


=head2 exportsw --format [txt|dot]

This command print the content of the switch database. There is actually two format. One is just txt for terminal and the other is the dot format from the graphviz environnement.

 klask exportsw --format dot > /tmp/map.dot
 dot -Tpng /tmp/map.dot > /tmp/map.png



=head1 CONFIGURATION

Because klask need many parameters, it's not possible actually to use command line parameters. The configuration is done in a /etc/klask/klask.conf YAML file. This format have many advantage over XML, it's easier to read and to write !

Here an example, be aware with indent, it's important in YAML, do not use tabulation !

 default:
   community: public
   snmpport: 161

 network:
   labnet:
     ip-subnet:
       - add: 192.168.1.0/24
       - add: 192.168.2.0/24
     interface: eth0
     main-router: gw1.labnet.local

   schoolnet:
     ip-subnet:
       - add: 192.168.6.0/24
       - add: 192.168.7.0/24
     interface: eth0.38
     main-router: gw2.schoolnet.local

 switch:
   - hostname: sw1.klask.local
     portignore:
       - 1
       - 2

   - hostname: sw2.klask.local
     location: BatK / 2 / K203
     type: HP2424
     portignore:
       - 1
       - 2

I think it's pretty easy to understand. The default section can be overide in any section, if parameter mean something in theses sections. Network to be scan are define in the network section. You must put a add by network. Maybe i will make a delete line to suppress specific computers. The switch section define your switch. You have to write the port number to ignore, this is important if your switchs are cascade. Juste put the ports numbers between switch.


=head1 FILES

 /etc/klask/klask.conf
 /var/lib/klask/klaskdb
 /var/lib/klask/switchdb

=head1 SEE ALSO

Net::SNMP, Net::Netmask, Net::CIDR::Lite, NetAddr::IP, YAML


=head1 VERSION

$Id: klask 178 2016-08-24 15:55:17Z g7moreau $


=head1 AUTHOR

Written by Gabriel Moreau, Grenoble - France


=head1 LICENSE AND COPYRIGHT

GPL version 2 or later and Perl equivalent

Copyright (C) 2005-2016 Gabriel Moreau.
