#!/usr/bin/perl
#
# Module: vyatta-show-interfaces.pl
# 
# **** License ****
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
# 
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# This code was originally developed by Vyatta, Inc.
# Portions created by Vyatta are Copyright (C) 2007 Vyatta, Inc.
# All Rights Reserved.
# 
# Author: Stig Thormodsrud
# Date: February 2008
# Description: Script to display interface information
# 
# **** End License ****
#

use lib "/opt/vyatta/share/perl5/";

use Vyatta::Interface;
use Vyatta::Misc;
use Getopt::Long;
use POSIX;
use NetAddr::IP;

use strict;
use warnings;

#
# valid actions
#
my %action_hash = (
    'allowed'       => \&run_allowed,
    'show'       => \&run_show_intf,
    'show-brief' => \&run_show_intf_brief,
    'show-count' => \&run_show_counters,
    'clear'      => \&run_clear_intf,
    'reset'      => \&run_reset_intf,
    );


my $clear_stats_dir = '/var/run/vyatta';
my $clear_file_magic = 'XYZZYX';

my @rx_stat_vars = 
    qw/rx_bytes rx_packets rx_errors rx_dropped rx_over_errors multicast/; 
my @tx_stat_vars = 
    qw/tx_bytes tx_packets tx_errors tx_dropped tx_carrier_errors collisions/;

sub get_intf_description {
    my $name = shift;
    my $description = interface_description($name);

    return "" unless $description;
    return $description;
}

sub get_intf_stats {
    my $intf = shift;
    
    my %stats = ();
    foreach my $var (@rx_stat_vars, @tx_stat_vars) {
	$stats{$var} = get_sysfs_value($intf, "statistics/$var");
    }
    return %stats;
}

sub get_intf_statsfile {
    my $intf = shift;

    return "$clear_stats_dir/$intf.stats";
}

sub get_clear_stats {
   my $intf = shift;

   my %stats = ();
   foreach my $var (@rx_stat_vars, @tx_stat_vars) {
       $stats{$var} = 0;
   }

   my $filename = get_intf_statsfile($intf);

   open (my $f, '<', $filename)
    or return %stats;

   my $magic = <$f>;
   chomp $magic;
   if ($magic ne $clear_file_magic) {
       print "bad magic [$intf]\n";
       return %stats;
   }

   my $timestamp = <$f>;
   chomp $timestamp;
   $stats{'timestamp'} = $timestamp;

   while (<$f>) {
       chop;
       my ($var, $val) = split(/,/);
       $stats{$var} = $val;
   }
   close($f);
   return %stats;
}

sub get_ipaddr {
    my $name = shift;
    
    # Skip IPV6 default Link-local
    return grep { !/^fe80/ } Vyatta::Misc::getIP($name);
}

sub get_state_link {
    my $name = shift;
    my $intf = new Vyatta::Interface($name);
    my $state;
    my $link = 'down';

    if ($intf->up()) {
	$state = 'up'; 
	$link = "up" if ($intf->running());
    } else {
	$state = "admin down";
    }

    return ($state, $link);
}

sub is_valid_intf {
    my $name = shift;
    return unless $name;

    my $intf = new Vyatta::Interface($name);
    return unless $intf;

    return $intf->exists();
}

sub get_intf_for_type {
    my $type = shift;
    my @interfaces = getInterfaces();
    my @list = ();
    foreach my $name (@interfaces) {
	if ($type) {
	    my $intf = new Vyatta::Interface($name);
	    next unless $intf;				# unknown type
	    next if ($type ne $intf->type());
	}
	push @list, $name;
    }

    return @list;
}

# Find vlan interfaces
sub get_vlan_intf() {
    my @interfaces = getInterfaces();
    my @list = ();

    foreach my $name (@interfaces) {
        my $intf = new Vyatta::Interface($name);
        next unless $intf && defined($intf->vif());
        push @list, $name;
    }

    return @list;
}

# Find vlan interfaces
sub get_vrrp_intf() {
    my @interfaces = getInterfaces();
    my @list = ();

    foreach my $name (@interfaces) {
        my $intf = new Vyatta::Interface($name);
        next unless $intf && defined($intf->vrid());
        push @list, $name;
    }

    return @list;
}

# This function has to deal with both 32 and 64 bit counters
sub get_counter_val {
    my ($clear, $now) = @_;

    return $now if $clear == 0;

    # device is using 64 bit values assume they never wrap
    my $value = $now - $clear;
    return $value if ($now >> 32) != 0;

    # The counter has rolled.  If the counter has rolled
    # multiple times since the clear value, then this math
    # is meaningless.
    $value = (4294967296 - $clear) + $now
	if ($value < 0);

    return $value;
}

#
# The "action" routines
#

sub run_allowed {
  my @intfs = @_;
  print "@intfs";
}

sub run_show_intf {
    my @intfs = @_;

    foreach my $intf (@intfs) {
	my %clear = get_clear_stats($intf);
	my $description = get_intf_description($intf);
	my $timestamp = $clear{'timestamp'};

	my $line = `ip addr show $intf | sed 's/^[0-9]*: //'`;
	chomp $line;

	if ($line =~ /link\/tunnel6/) {
	    my $estat = `ip -6 tun show $intf | sed 's/.*encap/encap/'`;
	    $line =~ s%    link/tunnel6%    $estat$&%;
	}

	print "$line\n";
	if (defined $timestamp and $timestamp ne "") {
	    my $time_str = strftime("%a %b %d %R:%S %Z %Y", 
				    localtime($timestamp));
	    print "    Last clear: $time_str\n";
	}
	if (defined $description and $description ne "") {
	    print "    Description: $description\n";
	}
	print "\n";

	my %stats = get_intf_stats($intf);

	my $fmt = "    %10s %10s %10s %10s %10s %10s\n";

	printf($fmt,
	       "RX:  bytes", "packets", "errors", "dropped", "overrun", "mcast");
	printf($fmt,
	       map { get_counter_val($clear{$_}, $stats{$_}) } @rx_stat_vars);

	printf($fmt,
	       "TX:  bytes", "packets", "errors", "dropped", "carrier", "collisions");
	printf($fmt,
	       map { get_counter_val($clear{$_}, $stats{$_}) } @tx_stat_vars);
    }
}

sub conv_brief_code {
  my $state = pop(@_);
  $state = 'u' if ($state eq 'up');
  $state = 'D' if ($state eq 'down');
  $state = 'A' if ($state eq 'admin down');
  return $state;
}

sub conv_descriptions {
  my $description = pop @_;
  my @descriptions;
  my $term_width = get_terminal_width;
  $term_width = 80 if (!defined($term_width));
  my $desc_len = $term_width - 56;
  my $line = '';
  foreach my $elem (split(' ', $description)){
    if ((length($line) + length($elem)) >= $desc_len){
      push(@descriptions, $line);
      $line = "$elem ";
    } else {
      $line .= "$elem ";
    }
  }
  push(@descriptions, $line);
  return @descriptions;
}

sub run_show_intf_brief {
    my @intfs = @_;
    my $format = "%-16s %-33s %-4s %s\n";
    my $format2 = "%-16s %s\n";
    print "Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down\n";
    printf($format, "Interface","IP Address","S/L","Description");
    printf($format, "---------","----------","---","-----------");
    foreach my $intf (@intfs) {
      my $interface = new Vyatta::Interface($intf);
      # ignore tunnels, unknown interface types, etc.
      next unless $interface;

      my $state = $interface->up() ? 'u' : 'A';
      my $link  = $interface->running() ? 'u' : 'D';

      my $description = $interface->description();
      my @descriptions = conv_descriptions($description)
	  if defined($description);

      my @ip_addr = get_ipaddr($intf);
      if (scalar(@ip_addr) == 0) {
        my $desc = '';
        $desc = shift @descriptions if (scalar(@descriptions) > 0 );
        printf($format, $intf, "-", "$state/$link", $desc);
        foreach my $descrip (@descriptions){
          printf($format, '', '', '', $descrip);
        }
      } else {
        my $tmpip = shift(@ip_addr);
        my $desc = '';
        if (length($tmpip) < 33){
          $desc = shift @descriptions if (scalar(@descriptions) > 0 );
          printf($format, $intf, $tmpip , "$state/$link", $desc);
          foreach my $descrip (@descriptions){
            printf($format, '', '', '', $descrip);
          }
          foreach my $ip (@ip_addr) {
            printf($format2, '', $ip) if (defined $ip);
          }
        } else {
          $desc = shift @descriptions if (scalar(@descriptions) > 0 );
          printf($format2, $intf, $tmpip);
          my $printed_desc = 0;
          foreach my $ip (@ip_addr) {
            if (length($ip) >= 33) {
              printf($format2, '', $ip) if (defined $ip);
            } else {
              if (!$printed_desc){
                printf($format, '', $ip, "$state/$link", $desc);
                $printed_desc = 1;
                foreach my $descrip (@descriptions){
                  printf($format, '', '', '', $descrip);
                }
              } else {
                printf($format2, '', $ip);
              }
            }
          }
          if (!$printed_desc){
            printf($format, '', '', "$state/$link", $desc);
            foreach my $descrip (@descriptions){
              printf($format, '', '', '', $descrip);
            }
          }
        }
      }
   }
}

sub run_show_counters {
    my @intfs = @_;

    printf("%-12s %10s %10s     %10s %10s\n",
	   "Interface","Rx Packets","Rx Bytes","Tx Packets","Tx Bytes");

    foreach my $intf (@intfs) {
	my ($state, $link) = get_state_link($intf);
	next if $state ne 'up';
	my %clear = get_clear_stats($intf);
	my %stats = get_intf_stats($intf);

	printf("%-12s %10s %10s     %10s %10s\n", $intf,
	       get_counter_val($clear{rx_packets}, $stats{rx_packets}),
	       get_counter_val($clear{rx_bytes},   $stats{rx_bytes}),
	       get_counter_val($clear{tx_packets}, $stats{tx_packets}),
	       get_counter_val($clear{tx_bytes},   $stats{tx_bytes})
	    );
    }
}

sub run_clear_intf {
    my @intfs = @_;

    foreach my $intf (@intfs) {
	my %stats = get_intf_stats($intf);
	my $filename = get_intf_statsfile($intf);

	mkdir $clear_stats_dir unless ( -d $clear_stats_dir );

	open(my $f, '>', $filename)
	    or die "Couldn't open $filename [$!]\n";

	print "Clearing $intf\n";
	print $f $clear_file_magic, "\n", time(), "\n";

	while (my ($var, $val) = each (%stats)) {
	    print $f $var, ",", $val, "\n";
	}

	close($f);
    }
}

sub run_reset_intf {
    my @intfs = @_;
    
    foreach my $intf (@intfs) {
	my $filename = get_intf_stats($intf);
	system("rm -f $filename");
    }
}

sub alphanum_split {
    my ($str) = @_;
    my @list = split m/(?=(?<=\D)\d|(?<=\d)\D)/, $str;
    return @list;
}

sub natural_order {
    my ($a, $b) = @_;
    my @a = alphanum_split($a);
    my @b = alphanum_split($b);
  
    while (@a && @b) {
	my $a_seg = shift @a;
	my $b_seg = shift @b;
	my $val;
	if (($a_seg =~ /\d/) && ($b_seg =~ /\d/)) {
	    $val = $a_seg <=> $b_seg;
	} else {
	    $val = $a_seg cmp $b_seg;
	}
	if ($val != 0) {
	    return $val;
	}
    }
    return @a <=> @b;
}

sub intf_sort {
    my @a = @_;
    my @new_a = sort { natural_order($a,$b) } @a;
    return @new_a;
}

sub usage {
    print "Usage: $0 [intf=NAME|intf-type=TYPE|vif|vrrp] action=ACTION\n";
    print "  NAME = ", join(' | ', get_intf_for_type()), "\n";
    print "  TYPE = ", join(' | ', Vyatta::Interface::interface_types()), "\n";
    print "  ACTION = ", join(' | ', keys %action_hash), "\n";
    exit 1;
}

#
# main
#
my ($intf_type, $intf, $vif_only, $vrrp_only);
my $action = 'show';

GetOptions("intf-type=s" => \$intf_type,
           "vif"         => \$vif_only,
           "vrrp"         => \$vrrp_only,
	   "intf=s"      => \$intf,
	   "action=s"    => \$action,
) or usage();

my @intf_list;

if ($intf) {
    die "Invalid interface [$intf]\n" 
	unless is_valid_intf($intf);

    push @intf_list, $intf;
} elsif ($intf_type) {
    @intf_list = get_intf_for_type($intf_type);
} elsif ($vif_only) {
    @intf_list = get_vlan_intf();
} elsif ($vrrp_only) {
    @intf_list = get_vrrp_intf();
} else {
    # get all interfaces
    @intf_list = get_intf_for_type();
}

@intf_list = intf_sort(@intf_list);

my $func;
if (defined $action_hash{$action}) {
    $func = $action_hash{$action};
} else {
    print "Invalid action [$action]\n";
    usage();
}

#
# make it so...
#
&$func(@intf_list);