#!/usr/bin/perl -w
#
# Copyright (C) 2005-2017 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>
#
# $Id: klask 314 2017-10-31 05:53:12Z g7moreau $

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

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';
use Digest::SHA qw(sha512_base64);
use Text::Table; # libtext-table-perl http://blogs.perl.org/users/steven_haryanto/2014/07/benchmarking-several-ascii-table-generator-modules.html

# 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 libtext-table-perl
# arping net-tools fping bind9-host arpwatch

################################################################
# general initialization
################################################################

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_LIST = @{$KLASK_CFG->{'switch'}};

my %SWITCH_LEVEL = ();
my %SWITCH_DB    = ();
LEVEL_OF_EACH_SWITCH:
for my $sw (@SWITCH_LIST) {
   $SWITCH_LEVEL{$sw->{'hostname'}} = $sw->{'level'} || $DEFAULT{'switch_level'}  || 2;
   $SWITCH_DB{$sw->{'hostname'}} = $sw;

   # SNMP parameter initialisation
   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->{'snmp_param_session'} = \%session;
   }
@SWITCH_LIST = 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,
   'poe-enable'           => \&cmd_poe_enable,
   'poe-disable'          => \&cmd_poe_disable,
   'poe-status'           => \&cmd_poe_status,
   'port-setvlan'         => \&cmd_port_setvlan,
   'port-getvlan'         => \&cmd_port_getvlan,
   'vlan-setname'         => \&cmd_vlan_setname,
   'vlan-getname'         => \&cmd_vlan_getname,
   'vlan-list'            => \&cmd_vlan_list,
   'host-setlocation'     => \&cmd_host_setlocation,
   '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',      revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J4120A           => { type => 1, model => 'HP1600M',        match => 'HP J4120A ProCurve Switch 1600M',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J9029A           => { type => 1, model => 'HP1800-8G',      match => 'PROCURVE J9029A',                     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J9449A           => { type => 1, model => 'HP1810-8G',      match => 'HP ProCurve 1810G - 8 GE',            revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J4093A           => { type => 1, model => 'HP2424M',        match => 'HP J4093A ProCurve Switch 2424M',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J9279A           => { type => 1, model => 'HP2510G-24',     match => 'ProCurve J9279A Switch 2510G-24',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J9280A           => { type => 1, model => 'HP2510G-48',     match => 'ProCurve J9280A Switch 2510G-48',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J4813A           => { type => 1, model => 'HP2524',         match => 'HP J4813A ProCurve Switch 2524',      revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J4900A           => { type => 1, model => 'HP2626A',        match => 'HP J4900A ProCurve Switch 2626',      revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J4900B           => { type => 1, model => 'HP2626B',        match => 'J4900B.+?Switch 2626',                revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   }, # ProCurve J4900B Switch 2626 # HP J4900B ProCurve Switch 2626
   J4899B           => { type => 1, model => 'HP2650',         match => 'ProCurve J4899B Switch 2650',         revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J9021A           => { type => 1, model => 'HP2810-24G',     match => 'ProCurve J9021A Switch 2810-24G',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J9022A           => { type => 1, model => 'HP2810-48G',     match => 'ProCurve J9022A Switch 2810-48G',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J8692A           => { type => 1, model => 'HP3500-24G',     match => 'J8692A Switch 3500yl-24G',            revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J4903A           => { type => 1, model => 'HP2824',         match => 'J4903A.+?Switch 2824,',               revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   J4110A           => { type => 1, model => 'HP8000M',        match => 'HP J4110A ProCurve Switch 8000M',     revision => qr{ProCurve .*?, revision (\w[\d\.]+?), ROM},   },
   JE074A           => { type => 2, model => 'HP5120-24G',     match => 'HP Comware.+?A5120-24G EI Switch',    revision => qr{Comware .*? Version (\d[\d\.]+?) Release},   },
   JE069A           => { type => 2, model => 'HP5120-48G',     match => 'HP Comware.+?A5120-48G EI Switch',    revision => qr{Comware .*? Version (\d[\d\.]+?) Release},   },
   JD377A           => { type => 2, model => 'HP5500-24G',     match => 'HP Comware.+?A5500-24G EI Switch',    revision => qr{Comware .*? Version (\d[\d\.]+?) Release},   },
   JD374A           => { type => 2, model => 'HP5500-24F',     match => 'HP Comware.+?A5500-24G-SFP EI ',      revision => qr{Comware .*? Version (\d[\d\.]+?) Release},   },
   # BayStack
   BS350T           => { type => 1, model => 'BS350T',         match => 'BayStack 350T HW'                     },
   # Nexans
   N3483G           => { type => 2, model => 'NA3483-6G',      match => 'GigaSwitch V3 TP SFP-I 48.+ ES3',     revision => qr{GigaSwitch .*?/SECURITY/V(\d[\d\.]+\w?)\)},  },
   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)
   # DELL
   PC7024           => { type => 2, model => 'DPC7024',        match => 'PowerConnect 7024,.+?VxWorks',        revision => qr{PowerConnect .*?, (\d[\d\.]+?), VxWorks},    },
   N2048            => { type => 2, model => 'DN2048',         match => 'Dell Networking N2048,',              revision => qr{Dell Networking .*?, (\d[\d\.]+?), Linux},   },
   N4032F           => { type => 2, model => 'DN4032F',        match => 'Dell Networking N4032F,',             revision => qr{Dell Networking .*?, (\d[\d\.]+?), Linux},   },
   N4064F           => { type => 2, model => 'DN4064F',        match => 'Dell Networking N4064F,',             revision => qr{Dell Networking .*?, (\d[\d\.]+?), Linux},   },
   # 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)
   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)
   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
   NApoeState      => '1.3.6.1.4.1.266.20.3.1.1.21',     # .NoPort = 2 (poe off)  = 8 (poe atHighPower) - Nexans
   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
   );

Readonly my %PORT_UPDOWN => (
   1 => 'enable',
   2 => 'disable',
   );

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 => $DEFAULT{'float-regex'} || qr{ ^float }xms;

Readonly my $SEP_AGGREGATOR_PORT => ',';  # : is already use to join switch and port
Readonly my $SEP_SWITCH_PORT     => ':';


################################################################
# main program
################################################################

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;

################################################################
# subroutine
################################################################

#---------------------------------------------------------------
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 $already   = shift || q{yes};

   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);
   #if ($ret{'ipv4_address'} !~ m/$RE_IPv4_ADDRESS/) {
   #   print "Error: for computer $param_ip_or_host on interface $interface, IP $ret{'ipv4_address'} is not valide\n";
   #   return %ret;
   #   }

   # perl -MSocket -E 'say scalar gethostbyaddr(inet_aton("194.254.66.240"), AF_INET)'
   my $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET) if $already eq 'yes';
   $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;
      }

   # ARP result
   #
   # LANG=C arp -a 194.254.66.62 -i eth331
   # gw66-62.legi.grenoble-inp.fr (194.254.66.62) at 00:08:7c:bb:0f:c0 [ether] on eth331
   #
   # LANG=C ip neigh show to 194.254.66.62 dev eth331
   # 194.254.66.62 lladdr 00:08:7c:bb:0f:c0 REACHABLE
   # LANG=C ip neigh show to 194.254.66.62
   # 194.254.66.62 dev eth331 lladdr 00:08:7c:bb:0f:c0 REACHABLE
#   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);
#      }
   if ($ret{'mac_address'} eq 'unknow') {
      # Last chance to have the mac_address
      if ($ret{'interface'} eq '*') {
         my $cmd_arp  = shell_command "ip neigh show to $ret{'ipv4_address'}";
         if ( $cmd_arp =~ m{ ^$RE_IPv4_ADDRESS \s dev \s ([\w\d\.\:]+) \s lladdr \s ( $RE_MAC_ADDRESS ) \s }xms ) {
            ($ret{'interface'}, $ret{'mac_address'}) = ($1, $2);
            }
         }
      else {
         my $cmd_arp  = shell_command "ip neigh show to $ret{'ipv4_address'} dev $ret{'interface'}";
         if ( $cmd_arp =~ m{ ^$RE_IPv4_ADDRESS \s lladdr \s ( $RE_MAC_ADDRESS ) \s }xms ) {
            $ret{'mac_address'} = $1;
            }
         }
      }

   # 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;
   }

#---------------------------------------------------------------
# Find Revision Firmware of a switch
sub get_switch_revision {
   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 ?
      last if not exists $SWITCH_KIND{$sw_kind}->{'revision'};

      my ($revision) = $sw_snmp_description =~ m/$SWITCH_KIND{$sw_kind}->{'revision'}/xms;
      return $revision || 'unknow';
      }

   return 'unknow';
   }

#---------------------------------------------------------------
# Get switch name and switch model
sub init_switch_names {
   my ($verbose, $verb_description, $check_hostname, $check_location) = @_;

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

   INIT_EACH_SWITCH:
   for my $sw (my @CLONE = @SWITCH_LIST) { # Make a local clone because some element can be deleted
      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
      print "$error \n" if $error;

      my $result = $session->get_request(
         -varbindlist => [
            $OID_NUMBER{'sysDescription'},
            $OID_NUMBER{'sysName'},
            $OID_NUMBER{'sysContact'},
            $OID_NUMBER{'sysLocation'},
            ]
         );
      if (!defined $result) {
         printf {*STDERR} "ERROR: %s.\n", $session->error();
         $session->close();
         # Remove bad switch
         @SWITCH_LIST = grep { $_->{'hostname'} ne $sw->{'hostname'} } @SWITCH_LIST;
         delete $SWITCH_LEVEL{$sw->{'hostname'}} if exists $SWITCH_LEVEL{$sw->{'hostname'}};
         delete $SWITCH_DB{$sw->{'hostname'}}    if exists $SWITCH_DB{$sw->{'hostname'}};
         next INIT_EACH_SWITCH;
         }

      $sw->{'description'} = $result->{$OID_NUMBER{'sysName'}} || $sw->{'hostname'};
      $sw->{'model'} = get_switch_model($result->{$OID_NUMBER{'sysDescription'}});
      $sw->{'revision'} = get_switch_revision($result->{$OID_NUMBER{'sysDescription'}});
      printf "%-26s 0--------->>>> %-25s %s\n", $sw->{'hostname'}, $sw->{'description'}, $sw->{'model'}.'('.$sw->{'revision'}.')' if $verbose;

      if ($verb_description) {
         my $desc = $result->{$OID_NUMBER{'sysDescription'}};
         $desc =~ s/[\n\r]/ /g;
         print " +> $sw->{'hostname'} - 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'};
         }

      $session->close;
      }

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

#---------------------------------------------------------------
# convert hexa (only 2 digits) to decimal
sub digit_hex2dec {
   #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 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;
   }

#---------------------------------------------------------------
# convert MAC hex address to decimal
sub mac_address_hex2dec {
   #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_hex2dec($_);
      }
   return $return;
   }

#---------------------------------------------------------------
sub format_aggregator4html {
   my $port_hr = shift;
   $port_hr =~ s/($SEP_AGGREGATOR_PORT)/: /; # First occurence
   $port_hr =~ s/($SEP_AGGREGATOR_PORT)/ /g; # Other occurence
   return $port_hr;
   }

#---------------------------------------------------------------
sub format_aggregator4dot {
   my $port_hr = shift;
   $port_hr =~ s/($SEP_AGGREGATOR_PORT)/ - /; # First occurence
   $port_hr =~ s/($SEP_AGGREGATOR_PORT)/ /g; # Other occurence
   return $port_hr;
   }

#---------------------------------------------------------------
# return the port and the switch where the computer is connected
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_id'} = '0';

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

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

   my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
   my $oid_search_port2 = $OID_NUMBER{'searchPort2'} .'.'. $vlan_id . mac_address_hex2dec($mac_address);

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

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

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

      my $swport_id = $result->{$oid_search_port1};
      my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);

      $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;
         my ($swport_hr_limited) = split /$SEP_AGGREGATOR_PORT/, $swport_hr; # Beginning of the swith port (Aggregator)
         next LOOP_ON_SWITCH if $swport_hr_limited eq $portignore;
         }

      $ret{'switch_hostname'}    = $sw->{'hostname'};
      $ret{'switch_description'} = $sw->{'description'};
      $ret{'switch_port_id'}     = $swport_id;
      $ret{'switch_port_hr'}     = $swport_hr; # human readable

      last LOOP_ON_SWITCH;
      }
   return %ret;
   }

#---------------------------------------------------------------
# search all the port on all the switches where the computer is detected
sub find_all_switch_port {
   my $mac_address = shift;
   my $vlan_id     = shift || 0;

   my $ret = {};

   return $ret if $mac_address eq 'unknow';

   my $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
   my $oid_search_port2 = $OID_NUMBER{'searchPort2'} .'.'. $vlan_id . mac_address_hex2dec($mac_address);
   LOOP_ON_ALL_SWITCH:
   for my $sw (@SWITCH_LIST) {
      my ($session, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
      print "$error \n" if $error;

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

      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
         my $swport_id = $result->{$oid_search_port1};
         my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);

         SWITCH_PORT_IGNORE:
         for my $portignore (@{$sw->{'portignore'}}) {
            if ($swport_hr eq $portignore) {
               $session->close;
               next LOOP_ON_ALL_SWITCH
               }
            }

         $ret->{$sw->{'hostname'}} = {};
         $ret->{$sw->{'hostname'}}{'hostname'}    = $sw->{'hostname'};
         $ret->{$sw->{'hostname'}}{'description'} = $sw->{'description'};
         $ret->{$sw->{'hostname'}}{'port_id'}     = $swport_id;
         $ret->{$sw->{'hostname'}}{'port_hr'}     = $swport_hr;
         }

      $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 0 if not exists $KLASK_CFG->{'network'}{$vlan_name};
   return $KLASK_CFG->{'network'}{$vlan_name}{'vlan-id'};
   }

#---------------------------------------------------------------
sub get_current_scan_mode {
   my $vlan_name = shift;

   return $KLASK_CFG->{'network'}{$vlan_name}{'scan-mode'} || $DEFAULT{'scan-mode'} || 'active';
   }

#---------------------------------------------------------------
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 normalize_port_human_readable {
   my $sw_port_hr  = shift;

   # Manufacturer abbreviation
   $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;

   # Customer abbreviation
   $sw_port_hr =~ s/^Ten/Te/i;
   $sw_port_hr =~ s/^Giga/Gi/i;

   return ucfirst $sw_port_hr;
   }

#---------------------------------------------------------------
sub snmp_get_rwsession {
   my ($sw) = @_;

   my %session = %{$sw->{'snmp_param_session'}};
   $session{-community} = $sw->{'community-rw'} || $DEFAULT{'community-rw'} || 'private';
   return %session;
   }

#---------------------------------------------------------------
sub snmp_get_switchport_id2hr {
   my ($snmp_session, $swport_id) = @_;

   # On H3C, port id (port_id) and port index (port_ix) are not the same
   # Double SNMP request to get the name
   # First get the index, second get the name (port_hr)

   my $oid_search_ix = $OID_NUMBER{'ifIndex'} .'.'. $swport_id;
   my $result_ix = $snmp_session->get_request(
      -varbindlist => [$oid_search_ix]
      );

   my $swport_ix = $swport_id;
   $swport_ix = $result_ix->{$oid_search_ix} if defined $result_ix;

   return snmp_get_switchport_ix2hr($snmp_session, $swport_ix);
   }

#---------------------------------------------------------------
sub snmp_get_switchport_ix2hr {
   my ($snmp_session, $swport_ix) = @_;

   my $oid_search_hr = $OID_NUMBER{'ifName'} .'.'. $swport_ix;
   my $result_hr = $snmp_session->get_request(
      -varbindlist => [$oid_search_hr]
      );
   my $swport_hr = $swport_ix;
   $swport_hr = normalize_port_human_readable($result_hr->{$oid_search_hr}) if defined $result_hr;

   # Aggregator port
   if ($swport_hr =~ m/^(Trk|Br|Po)/) {
      my $oid_search_index = $OID_NUMBER{'ifAggregator'}; # base OID
      my @args = ( -varbindlist =>  [$oid_search_index]);
      LOOP_ON_OID_PORT:
      while ( defined $snmp_session->get_next_request(@args) ) {
         my ($oid_current) = $snmp_session->var_bind_names;
         last LOOP_ON_OID_PORT if  not Net::SNMP::oid_base_match($oid_search_index, $oid_current);

         # IEEE8023-LAG-MIB::dot3adAggPortSelectedAggID.28 = INTEGER: 337
         # IEEE8023-LAG-MIB::dot3adAggPortAttachedAggID.28 = INTEGER: 337
         my $port_aggregator_index = $snmp_session->var_bind_list->{$oid_current};
         my ($current_port_ix) = reverse split /\./, $oid_current; # last number

         # prepare next loop item
         @args = (-varbindlist => [$oid_current]);

         next LOOP_ON_OID_PORT if $port_aggregator_index == 0;
         next LOOP_ON_OID_PORT if not $port_aggregator_index == $swport_ix;

         my $current_port_name = snmp_get_switchport_ix2hr($snmp_session, $current_port_ix);
         $swport_hr .= "$SEP_AGGREGATOR_PORT$current_port_name";
         }
      }
   return $swport_hr;
   }

#---------------------------------------------------------------
# Reverse search port number
sub snmp_get_switchport_hr2id {
   my ($snmp_session, $swport_hr, $verbose) = @_;

   # Split for Aggregator port
   # Keep only the Aggregator part
   ($swport_hr) = split /$SEP_AGGREGATOR_PORT/, $swport_hr;

   my $swport_id = $swport_hr;
   # direct return if already numeric (next loop is expensive) / old or simple switch
   return $swport_id if $swport_id =~ m/^\d+$/;

   my $oid_search_ix = $OID_NUMBER{'ifIndex'}; # base OID
   my @args = ( -varbindlist =>  [$oid_search_ix]);
   LOOP_ON_OID_PORT:
   while ( defined $snmp_session->get_next_request(@args) ) {
      my ($oid_current) = $snmp_session->var_bind_names;
      last LOOP_ON_OID_PORT if  not Net::SNMP::oid_base_match($oid_search_ix, $oid_current);

      my $port_ifIndex = $snmp_session->var_bind_list->{$oid_current};
      my ($port_ix) = reverse split /\./, $oid_current; # last number
      printf "PORT1: %s => %s\n", $oid_current, $port_ifIndex if $verbose;

      # prepare next loop item
      @args = (-varbindlist => [$oid_current]);

      my $oid_search_ifName = $OID_NUMBER{'ifName'} .'.'. $port_ifIndex;
      my $result = $snmp_session->get_request(-varbindlist => [$oid_search_ifName]);
      next LOOP_ON_OID_PORT if not defined $result;

      my $current_port_hr = normalize_port_human_readable($result->{$oid_search_ifName});
      printf "PORT2: $oid_search_ifName => $current_port_hr\n" if $verbose;
      if ($current_port_hr eq $swport_hr) {
         print "PORT3: $current_port_hr <-> $port_ix\n" if $verbose;

         # return port number ifIndex need by OID portUpDown
         $swport_id = $port_ifIndex; # other possible value could be $port_ix
         last LOOP_ON_OID_PORT;
         }
      }
   return $swport_id;
   }

#---------------------------------------------------------------
# Get the list of all the VLAN define on a switch
sub snmp_get_vlan_list {
   my ($snmp_session, $verbose) = @_;

   my %vlandb = (); # Hash vlan number => vlan name

   my $oid_search_index = $OID_NUMBER{'vlanName'}; # base OID
   my @args = ( -varbindlist =>  [$oid_search_index]);
   LOOP_ON_VLAN:
   while ( defined $snmp_session->get_next_request(@args) ) {
      my ($oid_current) = $snmp_session->var_bind_names;
      last LOOP_ON_VLAN if not Net::SNMP::oid_base_match($oid_search_index, $oid_current);

      my $vlan_name = $snmp_session->var_bind_list->{$oid_current};
      my ($vlan_id) = reverse split /\./, $oid_current; # last number
      printf "VLAN: %s => %s\n", $oid_current, $vlan_name if $verbose;

      $vlandb{$vlan_id} = $vlan_name;

      # prepare next loop item
      @args = (-varbindlist => [$oid_current]);
      }
   return %vlandb;
   }

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

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

      # Rename switch_port -> switch_port_id (2017/09/15)
      if (not exists $computerdb->{$ip}{'switch_port_id' and exists $computerdb->{$ip}{'switch_port'}}) {
         $computerdb->{$ip}{'switch_port_id'} = $computerdb->{$ip}{'switch_port'} if defined $computerdb->{$ip}{'switch_port'};
         $computerdb->{$ip}{'switch_port_id'} = 0 if $computerdb->{$ip}{'switch_port_id'} !~ m/^\d+$/; # force numeric
         }
      delete $computerdb->{$ip}{'switch_port'} if exists $computerdb->{$ip}{'switch_port'};

      next LOOP_ON_IP_ADDRESS if exists $computerdb->{$ip}{'switch_port_hr'} and defined $computerdb->{$ip}{'switch_port_hr'};

      $computerdb->{$ip}{'switch_port_hr'} = $computerdb->{$ip}{'switch_port_id'};
      }

   return $computerdb;
   }

#---------------------------------------------------------------
sub get_switchdb_checksum {
   my %switch_db = @_; # same as global %SWITCH_DB

   my $checksum_data = '';
   for my $sw_name (sort keys %switch_db) { # sort to always have the same order
      $checksum_data .= join ':',
         $switch_db{$sw_name}->{'description'},
         $switch_db{$sw_name}->{'model'},
         $switch_db{$sw_name}->{'hostname'},
         "\n";
      }

   return sha512_base64($checksum_data);
   }

#---------------------------------------------------------------
sub update_switchdb {
   my %args = (
      verbose => 0,
      @_);

   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_router ( get_list_main_router(get_list_network()) ) {
      print "Info: router loop $one_router\n" if $args{'verbose'};
      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 $args{'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_id'} eq '0';

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

   my %db_switch_link_with = ();

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

   my $timestamp = time;

   ALL_SWITCH:
   for my $one_switch (@list_all_switch) {
      my %resol_arp = resolve_ip_arp_host($one_switch, q{*}, q{low}); # arp resolution
      if (exists $SWITCH_DB{$one_switch}{'fake-ip'}) {
         my $fake_ip = $SWITCH_DB{$one_switch}{'fake-ip'};
         fast_ping($fake_ip);
         print "WARNING: fake ip on switch $one_switch -> $fake_ip / $resol_arp{'ipv4_address'}\n" if $args{'verbose'};
         my %resol_arp_alt = resolve_ip_arp_host($fake_ip, q{*}, q{low}); # arp resolution
         if ($resol_arp_alt{'mac_address'} ne 'unknow') {
            $resol_arp{'mac_address'}   = $resol_arp_alt{'mac_address'};
            $resol_arp{'interface'}     = $resol_arp_alt{'interface'};
            $resol_arp{'ipv4_address'} .= '*';
            # Force a MAC trace on switch
            system "arping -c 1 -w 1 -rR -i $resol_arp_alt{'interface'} $fake_ip > /dev/null 2>&1";
            }
         }
      print "Info: switch loop $one_switch\n" if $args{'verbose'};
      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 ($args{'verbose'}) {
         print "VERBOSE_3: $one_switch $resol_arp{'ipv4_address'} $resol_arp{'mac_address'}\n";
         print "VERBOSE_3: $one_switch --- ",
            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 $args{'verbose'};

      $SWITCH_DB{$one_switch}->{'ipv4_address'} = $resol_arp{'ipv4_address'};
      $SWITCH_DB{$one_switch}->{'mac_address'}  = $resol_arp{'mac_address'};
      $SWITCH_DB{$one_switch}->{'timestamp'}    = $timestamp;
      $SWITCH_DB{$one_switch}->{'network'}      = $vlan_name;
      }

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

      print "VERBOSE_5: loop on $db_switch_ip_hostnamefq{$ip}\n" if $args{'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 $args{'verbose'};

         next if $switch->{'port_id'}  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 $args{'verbose'};
         }

      }

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

   while ($maybe_more_than_one_switch_connected eq 'yes' and $cloop < 100) {
      $cloop++;
      print "VERBOSE_9: cloop reduction step: $cloop\n" if $args{'verbose'};
      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$SEP_SWITCH_PORT$port_hr"} ||= {};
            $db_switch_connected_on_port{"$connect$SEP_SWITCH_PORT$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/ $SEP_SWITCH_PORT /xms, $swport, 2;
         my @sw_on_same_port = keys %{$db_switch_connected_on_port{$swport}};
         print "VERBOSE_10: $swport -- ".$#sw_on_same_port." -- @sw_on_same_port\n" if $args{'verbose'};

         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$SEP_SWITCH_PORT$port_hr"} ||= {};
         $db_switch_connected_on_port{"$connect$SEP_SWITCH_PORT$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/ $SEP_SWITCH_PORT /xms, $swport, 2;
      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,
      timestamp         => $timestamp,
      checksum          => get_switchdb_checksum(%SWITCH_DB),
      };

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

################################################################
# command
################################################################

#---------------------------------------------------------------
sub cmd_help {

print <<'END';
klask - port and search manager for switches, map management

 klask version
 klask help

 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
 klask exportdb [--format|-f txt|html]
 klask removedb IP* computer*
 klask cleandb  [--verbose|-v] --day number_of_day --repair-dns

 klask updatesw [--verbose|-v]
 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY]

 klask searchdb [--kind|-k host|mac] computer [mac-address]
 klask search   computer
 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr

 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]

 klask bad-vlan-id [--day|-d days_before_alert]

 klask enable  [--verbose|-v] switch port
 klask disable [--verbose|-v] switch port
 klask status  [--verbose|-v] switch port

 klask poe-enable  [--verbose|-v] switch port
 klask poe-disable [--verbose|-v] switch port
 klask poe-status  [--verbose|-v] switch port

 klask vlan-getname switch vlan-id
 klask vlan-list switch
END
   return;
   }

#---------------------------------------------------------------
sub cmd_version {

print <<'END';
klask - port and search manager for switches, map management
Copyright (C) 2005-2017 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>

END
   print ' $Id: klask 314 2017-10-31 05:53:12Z 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, $no_rebuildsw);

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

   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

   {
   my $switch_checksum = get_switchdb_checksum(%SWITCH_DB);
   # Remplis le champs portignore des ports d'inter-connection pour chaque switch
   my $switch_connection = YAML::Syck::LoadFile("$KLASK_SW_FILE");
   if ($switch_checksum ne $switch_connection->{'checksum'}) { # Verify checksum
      if ($no_rebuildsw) {
         print "WARNING: switch database is outdate, please rebuild if with updatesw command\n";
         }
      else {
         print "WARNING: switch database is going to be rebuilt\n";
         update_switchdb(verbose => $verbose)
         }
      }

   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/ $SEP_SWITCH_PORT /xms, $swport, 2;
      $db_switch_chained_port{$sw_connect} .= "$port_connect:";
      }
   for my $sw (@SWITCH_LIST) {
      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 $current_net (@network) {

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

      fast_ping(@computer) if get_current_scan_mode($current_net) eq 'active';

      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 $already_exist = exists $computerdb->{$one_computer} ? 'yes' : 'no';
         my %resol_arp = resolve_ip_arp_host($one_computer, $current_interface, 'fast', $already_exist);

         # do not search on router connection (why ?)
         if ( exists $router_mac_ip{$resol_arp{'mac_address'}}) {
            $computer_not_detected{$one_computer} = $current_net;
            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_net;
            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_net;
            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_id     => $where{'switch_port_id'},
               switch_port_hr     => $where{'switch_port_hr'},
               timestamp          => $timestamp,
               network            => $current_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_id     => $where{'switch_port_id'},
               switch_port_hr     => $where{'switch_port_hr'},
               timestamp          => $resol_arp{'timestamp'},
               network            => $current_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_net 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 $current_net = $computer_not_detected{$one_computer};
      my $current_interface = get_current_interface($current_net);
      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';
      }
   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 $repairdns;
   my $verbose;
   my $database_has_changed;

   GetOptions(
      'day|d=i'   => \$days_to_clean,
      'verbose|v' => \$verbose,
      'repair-dns|r' => \$repairdns,
      );

   my @vlan_name = get_list_network();

   my $computerdb = computerdb_load();
   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 ($repairdns) { # Search and update unkown computer in reverse DNS
      LOOP_ON_IP_ADDRESS:
      for my $ip (keys %{$computerdb}) {
         next LOOP_ON_IP_ADDRESS if $computerdb->{$ip}{'hostname_fq'} ne 'unknow';

         my $packed_ip = scalar gethostbyname($ip);
         next LOOP_ON_IP_ADDRESS if not defined $packed_ip;

         my $hostname_fq = scalar gethostbyaddr($packed_ip, AF_INET);
         next LOOP_ON_IP_ADDRESS if not defined $hostname_fq;

         $computerdb->{$ip}{'hostname_fq'} = $hostname_fq;
         $database_has_changed++;
         }
      }

   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 "%-28s %8s              %-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})) {

      # 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;

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

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

      printf "%-28s %8s %12s %-40s %-15s %-18s %-16s %s\n",
         $computerdb->{$ip}{'switch_hostname'} || $computerdb->{$ip}{'switch_description'},
         $computerdb->{$ip}{'switch_port_hr'},
         $arrow,
         $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">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">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 $arrow ='&#10229;';
         $arrow ='&#10232;' 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 %06i' ,$switch_hostname, $computerdb->{$ip}{'switch_port_id'}; # Take switch index

      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'};

      my $parent_port_hr = format_aggregator4html($computerdb->{$ip}{'switch_port_hr'});

      print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$switch_hostname</td>
   <td class="bklask-port">$parent_port_hr</td>
   <td class="bklask-arrow">$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
#   <td colspan="2">$arrow</td>
      }

   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'}};

   # Output switch connection
   LOOP_ON_OUTPUT_SWITCH:
   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)/;
      my $arrow ='&#10236;';
         $arrow ='&#10238;' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;

      if (exists $db_switch_parent{$sw}) {
         # Link to uplink switch
         next LOOP_ON_OUTPUT_SWITCH;

         # Do not print anymore
         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; # \* for fake-ip

         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'};

         my $vlan = $db_switch{$db_switch_parent{$sw}->{'switch'}}->{'network'};
         $vlan .= ' ('.get_current_vlan_id($db_switch{$db_switch_parent{$sw}->{'switch'}}->{'network'}).')' if $db_switch{$db_switch_parent{$sw}->{'switch'}}->{'network'};

         my $parent_port_hr = format_aggregator4html($db_switch_output_port{$sw});
         my $child_port_hr  = format_aggregator4html($db_switch_parent{$sw}->{'port_hr'});

         print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$sw</td>
   <td class="bklask-port">$parent_port_hr</td>
   <td class="bklask-arrow">$arrow $child_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>$vlan</td>
   <td>$date</td>
  </tr>
END_HTML
         }
      else {
         # Router
         my $parent_port_hr = format_aggregator4html($db_switch_output_port{$sw});

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

         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; # \* for fake-ip

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

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

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

         print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="router">router</td>
   <td class="bklask-port"></td>
   <td class="bklask-arrow">$arrow $parent_port_hr</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>$vlan</td>
   <td>$date</td>
  </tr>
END_HTML
         #<td>$arrow</td><td>$parent_port_hr</td>

         next LOOP_ON_OUTPUT_SWITCH;

         # Old print
         print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$sw</td>
   <td class="bklask-port">$parent_port_hr</td>
   <td class="bklask-arrow">$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
         }
      }

   # Child switch connection : parent <- child
   for my $swport (sort keys %db_switch_connected_on_port) {
      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
      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; # \* for fake-ip

         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)/;
         my $arrow ='&#10235;';
            $arrow ='&#10237;' if $port_connect =~ m/^(Trk|Br|Po)/;

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

         if (exists $db_switch_output_port{$sw}) {

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

            my $parent_port_hr = format_aggregator4html($port_connect);
            my $child_port_hr  = format_aggregator4html($db_switch_output_port{$sw});

            print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
   <td class="bklask-port">$parent_port_hr</td>
   <td class="bklask-arrow">$arrow $child_port_hr</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>$vlan</td>
   <td>$date</td>
  </tr>
END_HTML
            }
         else {
            my $parent_port_hr = format_aggregator4html($port_connect);

            print <<"END_HTML";
  <tr class="$typerow">
   <td sorttable_customkey="$switch_hostname_sort">$sw_connect</td>
   <td class="bklask-port">$parent_port_hr</td>
   <td class="bklask-arrow">$arrow</td>
   <td sorttable_customkey="$sw">$sw</td>
   <td sorttable_customkey="">$ipv4_address</td>
   <td sorttable_customkey="">$mac_address</td>
   <td>$vlan</td>
   <td>$date</td>
  </tr>
END_HTML
            }
         }
      }

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

#---------------------------------------------------------------
sub cmd_bad_vlan_id {
   @ARGV = @_;

   my $days_before_alert = $DEFAULT{'days-before-alert'} || 15;
   my $verbose;

   GetOptions(
      'day|d=i'   => \$days_before_alert,
      );

   test_maindb_environnement();

   my $computerdb = computerdb_load();

   # create a database with the most recent computer by switch port
   my %switchportdb = ();
   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_id'} 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'};
      $switchportdb{$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 = $switchportdb{$swpt}->{'timestamp'};
      $ip_timestamp_mod -= $days_before_alert * 24 * 3600 if $ip_hostname_fq =~ m/$RE_FLOAT_HOSTNAME/;
      $ip_timestamp_ref -= $days_before_alert * 24 * 3600 if $switchportdb{$swpt}->{'hostname_fq'} =~ m/$RE_FLOAT_HOSTNAME/;

      if ($ip_timestamp_mod > $ip_timestamp_ref) {
         $switchportdb{$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 %switchportdb) {
      next LOOP_ON_RECENT_COMPUTER if $swpt =~ m/^\s*0$/;
      next LOOP_ON_RECENT_COMPUTER if $switchportdb{$swpt}->{'hostname_fq'} !~ m/$RE_FLOAT_HOSTNAME/;

      my $src_ip = $switchportdb{$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  $switchportdb{$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 $switchportdb{$swpt}->{'timestamp'} < $src_timestamp;

      my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime $switchportdb{$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, $switchportdb{$swpt}->{'vlan'}, $computerdb->{$src_ip}{'network'}, $vlan_id,
         $date,
         $src_date,
         $computerdb->{$src_ip}{'mac_address'},
         $computerdb->{$src_ip}{'hostname_fq'};
      }
   }

#---------------------------------------------------------------
sub cmd_poe_enable {
   @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 poe-enable 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 $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number

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

      my $result = $session->set_request(
         -varbindlist => [$oid_search, INTEGER, 8], # Only NEXANS
         );
      print $session->error()."\n" if $session->error_status();

      $session->close;
      }
   cmd_poe_status($switch_name, $switch_port);
   return;
   }

#---------------------------------------------------------------
sub cmd_poe_disable {
   @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 poe-disable 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 $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number

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

      my $result = $session->set_request(
         -varbindlist => [$oid_search, INTEGER, 2], # Only NEXANS
         );
      print $session->error()."\n" if $session->error_status();

      $session->close;
      }
   cmd_poe_status($switch_name, $switch_port);
   return;
   }

#---------------------------------------------------------------
sub cmd_poe_status {
   @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 poe-status 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 $oid_search = $OID_NUMBER{'NApoeState'} . ".$switch_port"; # Only NEXANS switch and low port number

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

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

      if (defined $result and $result->{$oid_search} ne 'noSuchInstance') {
         my $poe_status = $result->{$oid_search} || 'empty';
         $poe_status =~ s/8/enable/;
         $poe_status =~ s/2/disable/;
         printf "%s  %s poe %s\n", $sw_name, $switch_port, $poe_status;
         }
      else {
         print "Klask do not find PoE status on switch $sw_name on port $switch_port\n";
         }

      $session->close;
      }
   return;
   }

#---------------------------------------------------------------
sub cmd_host_setlocation {
   @ARGV = @_;

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

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

   if ($switch_name eq q{} or $switch_location eq q{}) {
      die "Usage: klask host-setlocation SWITCH_NAME LOCATION\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 $oid_search = $OID_NUMBER{'sysLocation'};

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

      my $result = $session->set_request(
         -varbindlist => [$oid_search, OCTET_STRING, $switch_location],
         );
      print $session->error()."\n" if $session->error_status();

      $session->close;
      }
   return;
   }

#---------------------------------------------------------------
# not finish - do not use
sub cmd_port_setvlan {
   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_LIST ) 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 $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
      my $oid_search_port2 = $OID_NUMBER{'searchPort2'} .'.'. 0 . mac_address_hex2dec($mac_address);
      print "Klask search OID $oid_search_port1 on switch $sw_name\n";
      print "Klask search OID $oid_search_port2 on switch $sw_name\n";

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

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

      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
         my $swport = $result->{$oid_search_port1};
         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_port_getvlan {
   @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 port-getvlan 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 $oid_search = $OID_NUMBER{'vlanPortDefault'} . ".$switch_port";

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

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

      if (defined $result and $result->{$oid_search} ne 'noSuchInstance') {
         my $vlan_id = $result->{$oid_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_vlan_setname {
   }

#---------------------------------------------------------------
# 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, $error) = Net::SNMP->session(snmp_get_rwsession($sw));
      print "$error \n" if $error;

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

      $session->close;
      }
   return;
   }

#---------------------------------------------------------------
sub cmd_vlan_getname {
   my $switch_name = shift || q{};
   my $vlan_id     = shift || q{};

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

   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST ) 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 $oid_search_vlan_name = $OID_NUMBER{'vlanName'} . ".$vlan_id";

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

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

      if (defined $result and $result->{$oid_search_vlan_name} ne 'noSuchInstance') {
         my $vlan_name = $result->{$oid_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_vlan_list {
   my $switch_name = shift || q{};

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

   $switch_name = join(',', map {$_->{'hostname'}} @SWITCH_LIST ) 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, $error) = Net::SNMP->session( %{$sw->{'snmp_param_session'}} );
      print "$error \n" if $error;

      my %vlandb = snmp_get_vlan_list($session);
      $session->close;

      print "VLAN_ID - VLAN_NAME # $sw_name\n";
      for my $vlan_id (keys %vlandb) {
         printf "%7i - %s\n", $vlan_id, $vlandb{$vlan_id};
         }
      }
   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_LIST) {
         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 = @_;

   my $days_to_death = $DEFAULT{'days-to-death'} || 365 * 2;
   my $format = 'txt';
   my $verbose;

   GetOptions(
      'day|d=i'      => \$days_to_death,
      '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_death);

   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 and get_current_scan_mode($vlan) eq 'active';

         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)) {
         my $vlan_nameid = $result_ip{$ip}->{'vlan'}.'('.get_current_vlan_id($result_ip{$ip}->{'vlan'}).')';
         printf "%-15s %-40s %-16s %s\n", $ip, $result_ip{$ip}->{'hostname_fq'}, $result_ip{$ip}->{'date_last_detection'}, $vlan_nameid;
      }
   }

#---------------------------------------------------------------
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'};

      my $vlan_nameid = $result_ip{$ip}->{'vlan'}.'('.get_current_vlan_id($result_ip{$ip}->{'vlan'}).')';

      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>$vlan_nameid</td>
   <td>$result_ip{$ip}->{'date_last_detection'}</td>
  </tr>
END_HTML
      }
   print <<'END_HTML';
 </tbody>
</table>
END_HTML
   }

#---------------------------------------------------------------
sub cmd_enable {
   @ARGV = @_;

   my $verbose;

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

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

   if ($switch_name eq q{} or $port_hr eq q{}) {
      die "Usage: klask disable SWITCH_NAME PORT\n";
      }

   if (not defined $SWITCH_DB{$switch_name}) {
      die "Switch $switch_name must be defined in klask configuration file\n";
      }

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

   # Retrieve numeric port value
   my $port_id = snmp_get_switchport_hr2id($session, normalize_port_human_readable($port_hr), $verbose ? 'yes' : '');
   die "Error : Port $port_hr does not exist on switch $switch_name\n" if not $port_id =~ m/^\d+$/;

   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} .'.'. $port_id;
   print "Info: switch $switch_name port $port_hr SNMP OID $oid_search_portstatus\n" if $verbose;

   my $result = $session->set_request(
      -varbindlist => [$oid_search_portstatus, INTEGER, 1],
      );
   print $session->error()."\n" if $session->error_status();

   $session->close;

   #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 {
   @ARGV = @_;

   my $verbose;

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

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

   if ($switch_name eq q{} or $port_hr eq q{}) {
      die "Usage: klask disable SWITCH_NAME PORT\n";
      }

   if (not defined $SWITCH_DB{$switch_name}) {
      die "Switch $switch_name must be defined in klask configuration file\n";
      }

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

   # Retrieve numeric port value
   my $port_id = snmp_get_switchport_hr2id($session, normalize_port_human_readable($port_hr), $verbose ? 'yes' : '');
   die "Error : Port $port_hr does not exist on switch $switch_name\n" if not $port_id =~ m/^\d+$/;

   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} .'.'. $port_id;
   print "Info: switch $switch_name port $port_hr SNMP OID $oid_search_portstatus\n" if $verbose;

   my $result = $session->set_request(
      -varbindlist => [$oid_search_portstatus, INTEGER, 2],
      );
   print $session->error()."\n" if $session->error_status();

   $session->close;

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

#---------------------------------------------------------------
sub cmd_status {
   @ARGV = @_;

   my $verbose;

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

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

   if ($switch_name eq q{} or $port_hr eq q{}) {
      die "Usage: klask status SWITCH_NAME PORT\n";
      }

   if (not defined $SWITCH_DB{$switch_name}) {
      die "Switch $switch_name must be defined in klask configuration file\n";
      }

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

   # Retrieve numeric port value
   my $port_id = snmp_get_switchport_hr2id($session, normalize_port_human_readable($port_hr), $verbose ? 'yes' : '');
   die "Error : Port $port_hr does not exist on switch $switch_name\n" if not $port_id =~ m/^\d+$/;

   my $oid_search_portstatus = $OID_NUMBER{'portUpDown'} .'.'. $port_id;
   print "Info: switch $switch_name port $port_hr ($port_id) SNMP OID $oid_search_portstatus\n" if $verbose;

   my $result = $session->get_request(
      -varbindlist => [$oid_search_portstatus]
      );
   print $session->error()."\n" if $session->error_status();
   if (defined $result) {
      print "$PORT_UPDOWN{$result->{$oid_search_portstatus}}\n";
      }

   $session->close;

   #system "snmpget -v 1 -c public $switch_name 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_LIST ) if $switch_name eq q{*} or $switch_name eq q{all};

   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 $oid_search_port1 = $OID_NUMBER{'searchPort1'} . mac_address_hex2dec($mac_address);
      my $oid_search_port2 = $OID_NUMBER{'searchPort2'} .'.'. $vlan_id . mac_address_hex2dec($mac_address);
      print "Klask search OID $oid_search_port1 on switch $sw_name\n" if $verbose;
      print "Klask search OID $oid_search_port2 on switch $sw_name\n" if $verbose;

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

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

      if (defined $result and $result->{$oid_search_port1} ne 'noSuchInstance') {
         my $swport_id = $result->{$oid_search_port1};
         my $swport_hr = snmp_get_switchport_id2hr($session, $swport_id);
         print "Klask find MAC $mac_address on switch $sw_name port $swport_hr\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,
      );
   
   update_switchdb(verbose => $verbose);
   }

#---------------------------------------------------------------
sub cmd_exportsw {
   @ARGV = @_;

   test_switchdb_environnement();

   my $format = 'txt';
   my $graph_modulo = 0;
   my $graph_shift  = 1;

   GetOptions(
      'format|f=s'  => \$format,
      'modulo|m=i'  => \$graph_modulo,
      'shift|s=i'   => \$graph_shift,
      );

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

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

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

#---------------------------------------------------------------
sub cmd_exportsw_txt {
   my %args = (
      way   => 'all',
      list  => 0,
      @_);

   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'}};

   # Switch output port and parent port connection
   my $tb_child = Text::Table->new( # http://www.perlmonks.org/?node_id=988320
      {align => 'left',   align_title => 'left',   title => 'Child_Switch'},
      {align => 'right',  align_title => 'right',  title => 'Output_Port'},
      {align => 'center', align_title => 'center', title => 'Link'},
      {align => 'left',   align_title => 'left',   title => 'Input_Port'},
      {align => 'left',   align_title => 'left',   title => 'Parent_Switch'},
      );
   for my $sw (sort keys %db_switch_output_port) {
      my $arrow ='--->';
         $arrow ='===>' if $db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/;
      if (exists $db_switch_parent{$sw}) {
         $tb_child->add($sw, $db_switch_output_port{$sw}, $arrow, $db_switch_parent{$sw}->{'port_hr'}, $db_switch_parent{$sw}->{'switch'});

         }
      else {
         $tb_child->add($sw, $db_switch_output_port{$sw}, $arrow, '', 'router');
         }
      }
   my @colrange = map { scalar $tb_child->colrange($_) } (0 .. 4); # force scaler context
   $tb_child->add(map { ' ' x $_ } reverse @colrange); # add empty line to force symetric table output
   print $tb_child->title();
   print $tb_child->rule('-');
   print $tb_child->body(0, $tb_child->body_height()-1); # remove last fake line
   $tb_child->clear;

   # Switch parent and children port inter-connection
   print "\n";
   my $tb_parent = Text::Table->new( # http://www.perlmonks.org/?node_id=988320
      {align => 'left',   align_title => 'left',   title => 'Parent_Switch'},
      {align => 'right',  align_title => 'right',  title => 'Input_Port'},
      {align => 'center', align_title => 'center', title => 'Link'},
      {align => 'left',   align_title => 'left',   title => 'Output_Port'},
      {align => 'left',   align_title => 'left',   title => 'Child_Switch'},
      );
   for my $swport (sort keys %db_switch_connected_on_port) {
      my ($sw_connect, $port_connect) = split m/ $SEP_SWITCH_PORT /xms, $swport, 2;
      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
         my $arrow ='<---';
            $arrow ='<===' if $port_connect =~ m/^(Trk|Br|Po)/;
         if (exists $db_switch_output_port{$sw}) {
            $tb_parent->add($sw_connect, $port_connect, $arrow, $db_switch_output_port{$sw}, $sw);
            }
         else {
            $tb_parent->add($sw_connect, $port_connect, $arrow, '', $sw);
            }
         }
      }
   @colrange = map { scalar $tb_parent->colrange($_) } (0 .. 4); # force scaler context
   $tb_parent->add(map { ' ' x $_ } reverse @colrange); # add empty line to force symetric table output
   print $tb_parent->title();
   print $tb_parent->rule('-');
   print $tb_parent->body(0, $tb_parent->body_height()-1); # remove last fake line
   $tb_parent->clear;

   return;
   }

#---------------------------------------------------------------
sub cmd_exportsw_dot {
   my $graph_modulo = shift;
   my $graph_shift  = shift;

   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 $timestamp                   =   $switch_connection->{'timestamp'};

   my $invisible_node = 0; # Count number of invisible node

   my %db_building= ();
   my %db_switch_line = (); # Number of line drawed on a switch
   for my $sw (values %db_switch_global) {
      my ($building, $location) = split m/ \/ /xms, $sw->{'location'}, 2;
      $db_building{$building} ||= {};
      $db_building{$building}->{$location} ||= {};
      $db_building{$building}->{$location}{ $sw->{'hostname'} } = 'y';

      $db_switch_line{$sw} = 0;
      }


   print "digraph G {\n";
   print "rankdir=LR;\n";
   #print "splines=polyline;\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 ($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;
   print "\"$date\" [label=\"MAP DATE\\n\\n$date\", color=white, fillcolor=black, shape=polygon, sides=14, style=filled, fontcolor=white];\n";
   print "site -> \"$date\" [style=invis];\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}}) {

            my $peripheries = 1;
            my $color = 'lightblue';
            if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
               $peripheries = 2;
               $color = "\"$color:$color\"";
               }
            print "\"$sw:$db_switch_output_port{$sw}\" [label=\"".format_aggregator4dot($db_switch_output_port{$sw})."\", color=black, fillcolor=lightblue, peripheries=$peripheries, 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, color=$color, arrowhead=normal, arrowtail=invdot];\n";


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

               #$db_switch_line{$sw}++;
               #if ($db_switch_line{$sw} % 9 == 0) {
               #   # Create invisible node
               #   $invisible_node++;
               #   my $invisible = '__Invisible_' . $invisible_node;
               #   print "$invisible [shape=none, label=\"\"]\n";
               #   print "\"$sw:$port_connect\" -> $invisible [style=invis]\n";
               #   print "$invisible            -> \"$sw\"    [style=invis]\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_id'};
         }
      else {
         my $style = 'solid';
         my $color = 'black'; # navyblue
         if ($db_switch_output_port{$sw} =~ m/^(Trk|Br|Po)/) {
            $style = 'bold';
            $color = "\"$color:invis:$color\"";
            }
         printf "   \"%s:%s\" -> internet [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw};
         }
      }
   print "\n";

   # shift graph between 1 or 2 when $graph_shift = 3
   my $graph_breaker = 1;

#   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/ $SEP_SWITCH_PORT /xms, $swport, 2;
      for my $sw (keys %{$db_switch_connected_on_port{$swport}}) {
         my $style = 'solid';
         my $color = 'black'; # navyblue
         if ($port_connect =~ m/^(Trk|Br|Po)/) {
            $style = 'bold';
            $color = "\"$color:invis:$color\"";
            }
         if (exists $db_switch_output_port{$sw}) {
            printf "   \"%s:%s\" -> \"%s:%s\" [style=$style, color=$color];\n", $sw, $db_switch_output_port{$sw}, $sw_connect, $port_connect;

            next if $graph_modulo == 0; # No shift (invisible nodes) in graph
            $db_switch_line{$sw_connect}++;
            if ($db_switch_line{$sw_connect} % $graph_modulo == 0) {
               # Create invisible node
               $invisible_node++;
               my $invisible = '__Invisible_' . $invisible_node;
               print  "   \"$invisible.a\" [shape=none, label=\"\"];\n";
               printf "   \"%s:%s\"  -> \"$invisible.a\" [style=invis];\n", $sw, $db_switch_output_port{$sw};
               $graph_breaker++;
               if ($graph_shift == 2 or ($graph_shift == 3 and ($graph_breaker % 2) == 0)) {
                  # Two invisible node
                  print  "   \"$invisible.b\" [shape=none, label=\"\"];\n";
                  print  "   \"$invisible.a\" -> \"$invisible.b\" [style=invis];\n";
                  printf "   \"$invisible.b\" -> \"%s:%s\"  [style=invis];\n", $sw_connect, $port_connect;
                  }
               else {
                  # One invisible node
                  printf "   \"$invisible.a\" -> \"%s:%s\"  [style=invis];\n", $sw_connect, $port_connect;
                  }
               }
            }
         else {
            printf "   \"%s\"   -> \"%s:%s\" [style=$style];\n", $sw, $sw_connect, $port_connect;
            }
         }
      }

print "}\n";
   return;
   }


################################################################
# documentation
################################################################

__END__

=head1 NAME

klask - port and search manager for switches, map management


=head1 USAGE

 klask version
 klask help

 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]
 klask exportdb [--format|-f txt|html]
 klask removedb IP* computer*
 klask cleandb  [--verbose|-v] --day number_of_day --repair-dns

 klask updatesw [--verbose|-v]
 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY]

 klask searchdb [--kind|-k host|mac] computer [mac-address]
 klask search   computer
 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr

 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]

 klask bad-vlan-id [--day|-d days_before_alert]

 klask enable  [--verbose|-v] switch port
 klask disable [--verbose|-v] switch port
 klask status  [--verbose|-v] switch port

 klask poe-enable  [--verbose|-v] switch port
 klask poe-disable [--verbose|-v] switch port
 klask poe-status  [--verbose|-v] switch port

 klask vlan-getname switch vlan-id
 klask vlan-list switch


=head1 DESCRIPTION

Klask is a small tool to find where is connected a host in a big network
and on which VLAN.
Klask mean search in brittany.
No hight level protocol like CDL, LLDP are use.
Everything is just done with SNMP request on MAC address.

Limitation : loop cannot be detected and could be problematic when the map is created (C<updatesw> method).
If you use PVST or MSTP and create loop between VLAN,
you have to use C<portignore> functionality on switch port to cut manually loop
(see config file below).

When you use a management port to administrate a switch,
it's not possible to create the map with this switch because it does not have a MAC address,
so other switch cannot find the real downlink port...
One way to work around this problem is, if you have a computer directly connected on the switch,
to put this IP as the fake ip for the switch.
The MAC address associated will be use just for the map detection.
The C<fake-ip> parameter is defined in the config file.

Klask has now a web site dedicated for it: L<http://servforge.legi.grenoble-inp.fr/projects/klask>!


=head1 COMMANDS

Some command are defined in the source code but are not documented here.
Theses could be not well defined, not finished, not well tested...
You can read the source code and use them at your own risk
(like for all the Klask code).

=head2 search

 klask search   computer

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 search-mac-on-switch

 klask search-mac-on-switch [--verbose|-v] [--vlan|-i vlan-id] switch mac_addr

This command search a MAC address on a switch.
To search on all switch, you could put C<'*'> or C<all>.
The VLAN parameter could help.


=head2 enable

 klask enable  [--verbose|-v] switch port

This command activate a port (or an agrregate bridge port) on a switch by SNMP.
So you need to give the switch name and a port on the command line.
See L</ABBREVIATION FOR PORT>.

Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.


=head2 disable

 klask disable [--verbose|-v] switch port

This command deactivate a port (or an agrregate bridge port) on a switch by SNMP.
So you need to give the switch name and a port on the command line.
See L</ABBREVIATION FOR PORT>.

Warning: You need to have the SNMP write access on the switch in order to modify it's configuration.


=head2 status

 klask status  [--verbose|-v] switch port

This command return the status of a port number on a switch by SNMP.
The return value could be C<enable> or C<disable> word.
So you need to give the switch name and a port on the command line.
See L</ABBREVIATION FOR PORT>.

If it's not possible to change port status with command L</enable> and L</disable>
(SNMP community read write access),
it's always possible to have the port status even for bridge agrregate port.


=head2 updatedb

 klask updatedb [--verbose|-v] [--verb-description|-d] [--chk-hostname|-h] [--chk-location|-l] [--no-rebuildsw|-R]

This command will scan networks and update the computer database.
To know which are the cmputer scanned, you have to configure the file F</etc/klask/klask.conf>.
This file is easy to read and write because Klask use YAML format and not XML
(see L</CONFIGURATION>).

Option are not stable and could be use manually when you have a new kind of switch.
Maybe some option will be transfered in a future C<checksw> command!

The network parameter C<scan-mode> can have two values: C<active> or C<passive>.
By default, a network is C<active>.
This means that an C<fping> command is done at the beginning on all the IP of the network
and the computers that was not detected in this pass, but where their Klask entry is less than one week,
will have an C<arping>
(some OS do not respond to C<ping> but a computer have to respond to C<arping> if it want to interact with other).
In the scan mode C<passive>, no C<fping> and no C<arping> are done.
It's good for big subnet with few computer (telephone...).
The idea of the C<active> scan mode is to force computer to regulary send packet over the network.

At the beginning, the command verify that the switch map checksum is always valid.
Otherwise, a rebuild procedure will ne done automatically.

=head2 exportdb

 klask exportdb [--format|-f txt|html]

This command print the content of the computer database.
There is actually only two format : TXT and HTML.
By default, format is TXT.
It's very easy to have more format, it's just need times...

=head2 removedb

 klask removedb IP* computer*

This command remove an entry in the database.
There is only one kind of parameter, the IP of the computers to be removed.
You can put as many IP as you want...

Computer DNS names are also a valid entry because a DNS resolver is executed at the beginning.

=head2 cleandb

 klask cleandb  [--verbose|-v] --day number_of_day --repair-dns

Remove double entry (same MAC-Address) in the computer database when the older one is older than X day (C<--day>) the new one.
Computer name beginning by 'float' (regex C<^float>) are not really taken into account but could be remove.
This could be configure with the global regex parameter C<float-regex> in the configuration file F</etc/klask/klask.conf>.
This functionality could be use when computer define in VLAN 1
could have a float IP when they are connected on VLAN 2.
In the Klask database, the float DNS entries are less important.

When reverse DNS has not been done by the past, option C<--repair-dns> force a reverse DNS check on all unkown host.

=head2 updatesw

 klask updatesw [--verbose|-v]

This command build a map of your manageable switch on your network.
The list of the switches must be given in the file F</etc/klask/klask.conf> (see L</CONFIGURATION>).

The database has a checksum which depend of all the active switches.
It's use when rebuilding the database in case of change in switch configuration (one more for example).

=head2 exportsw

 klask exportsw [--format|-f txt|dot] [--modulo|-m XX] [--shift|-s YY]

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.
By default, format is TXT.

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

In case you have too many switch connected on one switch,
the graphviz result graph could be too much vertical.
With C<--modulo> > 0, you can specify how many switches (connected on one switch) are on the same columns
before shifting them to one column to the left and back again.
The C<--shift> parameter must be 1, 2 or 3.
With C<--shift> egual to 2, the shift will be to two column to the left.
With 3, it will be 1 to the left and 2 to the left one time over two !
In practise, we just add virtuals nodes in the dot file,
that means the result graph is generated with theses virtuals but invisibles nodes...

=head2 ip-free

 klask ip-free [--verbose|-v] [--day|-d days-to-death] [--format|-f txt|html] [vlan_name]

This command return IP address that was not use (detected by Klask) at this time.
The list returned could be limited to just one VLAN.
IP returned could have been never used or no computer have been detected since the number of days specified
(2 years by default).
This parameter could also be define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).

 default:
   days-to-death: 730

Computer that does not have the good IP but takes a float one (see L</cleandb>) are taken into account.


=head2 bad-vlan-id

 klask bad-vlan-id [--day|-d days_before_alert]

This command return a list of switch port that are not configure with the good VLAN.
Computer which are in bad VLAN are detected with the float regex parameter (see L</cleandb>)
and another prior trace where they had the good IP (good DNS name).
The computer must stay connected on a bad VLAN more than XX days (15 days by default) before alert.
This parameter could also define in the configuration file F</etc/klask/klask.conf> (see L</CONFIGURATION>).

 default:
   days-before-alert: 15

This functionality is not need if your switch use RADIUS 802.1X configuration...


=head2 poe-enable

 klask poe-enable  [--verbose|-v] switch port

This command activate the PoE (Power over Ethernet) on a switch port by SNMP.
So you need to give the switch name and a port on the command line.
See L</ABBREVIATION FOR PORT>.

Warning: Only NEXANS switches are supported (we do not have other switch for testing).
You need to have the SNMP write access on the switch in order to modify it's configuration.


=head2 poe-disable

 klask poe-disable [--verbose|-v] switch port

This command deactivate the PoE (Power over Ethernet) on a switch port by SNMP.
So you need to give the switch name and a port on the command line.
See L</ABBREVIATION FOR PORT>.

Warning: Only NEXANS switches are supported (we do not have other switch for testing).
You need to have the SNMP write access on the switch in order to modify it's configuration.


=head2 poe-status

 klask poe-status  [--verbose|-v] switch port

This command return the status of the PoE (Power over Ethernet) on a switch port by SNMP.
The return value could be C<enable> or C<disable> word.
So you need to give the switch name and a port on the command line.
See L</ABBREVIATION FOR PORT>.

If it's not possible to change the PoE status with command L</poe-enable> and L</poe-disable>
(SNMP community read write access),
it's always possible to have the PoE port status.

Warning: Only NEXANS switches are supported (we do not have other switch for testing).


=head1 CONFIGURATION

Because Klask need many parameters, it's not possible actually to use command line parameters for everything.
The configuration is done in a F</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
   community-rw: private
   snmpport: 161
   float-regex: '(?^msx: ^float )'
   scan-mode: active

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

   schoolnet:
     ip-subnet:
       - add: 192.168.3.0/24
       - add: 192.168.4.0/24
     interface: eth0.38
     vlan-id: 13
     main-router: gw2.schoolnet.local
     scan-mode: passive

   etunet:
     ip-subnet:
       - add: 192.168.5.0/24
     interface: eth2
     vlan-id: 14
     main-router: gw3.etunet.local
     scan-mode: passive

 switch:
   - hostname: sw1.klask.local
     location: BatY / 1 floor / K004
     portignore:
       - 1
       - 2

   - hostname: sw2.klask.local
     location: BatY / 2 floor / K203
     type: HP2424
     portignore:
       - 1
       - 2
     fake-ip: 192.168.9.14

   - hostname: sw3.klask.local
     location: BatY / 2 floor / K203

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 an 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 was important if your switchs are cascades
(right now, method C<updatesw> find them automatically)
and is still important if you have loop (with PVST or MSTP).
Just put the ports numbers between switch.

The C<community> parameter is use to get SNMP data on switch.
It could be overload for each switch.
By default, it's value is C<public> and you have to configure a readonly word for safety reason.
Some few command change the switch state as the commands L</enable> and L</disable>.
In theses rares cases, you need a readwrite SNMP community word define in your configuration file.
Klask then use since version C<0.6.2> the C<community-rw> parameter which by default is egal to C<private>.


=head1 ABBREVIATION FOR PORT

HP Procurve and Nexans switches have a simplistic numbering scheme.
It's just number: 1, 2, 3... 24.
On HP8000 chassis, ports names begin with an uppercase letter: A1, A2...
Nothing is done on theses ports names.

On HP Comware and DELL, port digitization schema use a port speed word (generally a very verbose word)
followed by tree number.
In order to have short name,
we made the following rules:

 Bridge-Aggregation     -> Br
 FastEthernet           -> Fa
 Forty-GigabitEthernet  -> Fo
 FortyGigabitEthernet   -> Fo
 GigabitEthernet        -> Gi
 Giga                   -> Gi
 Port-Channel           -> Po
 Ten-GigabitEthernet    -> Te
 TenGigabitEthernet     -> Te
 Ten                    -> Te

All Klask command automatically normalize the port name on standart output
and also on input command line.

In the case of use an aggregator port (Po, Tk, Br ...),
the real ports used are also return.


=head1 SWITCH SUPPORTED

Here is a list of switches where Klask gives or gave (for old switches) good results.
We have only a few manageable switches to actually test Klask.
It is quite possible that switches from other brands will work just as well.
You just have to do a test on it and add the line of description that goes well in the source code.
Contact us for any additional information.

In the following list, the names of the switch types written in parentheses are the code names returned by Klask.
This makes it possible to adjust the code names of the different manufacturers!

HP: J3299A(HP224M), J4120A(HP1600M), J9029A(HP1800-8G), J9449A(HP1810-8G), J4093A(HP2424M), J9279A(HP2510G-24),
J9280A(HP2510G-48), J4813A(HP2524), J4900A(HP2626A), J4900B(HP2626B), J4899B(HP2650), J9021A(HP2810-24G), J9022A(HP2810-48G),
J8692A(HP3500-24G), J4903A(HP2824), J4110A(HP8000M), JE074A(HP5120-24G), JE069A(HP5120-48G), JD377A(HP5500-24G), JD374A(HP5500-24F).

BayStack: BayStack 350T HW(BS350T)

Nexans: GigaSwitch V3 TP SFP-I 48V ES3(NA3483-6G), GigaSwitch V3 TP.PSE.+ 48/54V ES3(NA3483-6P)

DELL: PC7024(DPC7024), N2048(DN2048), N4032F(DN4032F), N4064F(DN4064F)

H3C and 3COM switches have never not been tested but the new HP Comware switches are exactly the same...

H3C: H3C5500

3COM: 3C17203, 3C17204, 3CR17562-91, 3CR17255-91, 3CR17251-91, 3CR17571-91, 3CRWX220095A, 3CR17254-91, 3CRS48G-24S-91,
3CRS48G-48S-91, 3C17708, 3C17709, 3C17707, 3CR17258-91, 3CR17181-91, 3CR17252-91, 3CR17253-91, 3CR17250-91, 3CR17561-91,
3CR17572-91, 3C17702-US, 3C17700.


=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

=over

=item * L<Web site|http://servforge.legi.grenoble-inp.fr/projects/klask>

=item * L<Online Manual|http://servforge.legi.grenoble-inp.fr/pub/klask/klask.html>

=back


=head1 VERSION

$Id: klask 314 2017-10-31 05:53:12Z g7moreau $


=head1 AUTHOR

Written by Gabriel Moreau, Grenoble - France


=head1 LICENSE AND COPYRIGHT

GPL version 2 or later and Perl equivalent

Copyright (C) 2005-2017 Gabriel Moreau <Gabriel.Moreau(A)univ-grenoble-alpes.fr>.
