# Wrappers for iproute2 utilities
#
# **** 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) 2008 Vyatta, Inc.
# All Rights Reserved.
# **** End License ****
package Vyatta::Qos::Util;
use strict;
use warnings;

our @EXPORT =
  qw(getRate getPercent getBurstSize getProtocol getDsfield getTime getAutoRate);
our @EXPORT_OK = qw(getIfIndex);
use base qw(Exporter);

sub get_num {
    use POSIX qw(strtod);
    my ($str) = @_;
    return unless defined($str);

    # remove leading/trailing spaces
    $str =~ s/^\s+//;
    $str =~ s/\s+$//;

    $! = 0;
    my ( $num, $unparsed ) = strtod($str);
    if ( ( $unparsed == length($str) ) || $! ) {
        return;    # undefined (bad input)
    }

    if ( $unparsed > 0 ) { return $num, substr( $str, -$unparsed ); }
    else                 { return $num; }
}

## get_rate("10mbit")
# convert rate specification to number
# from tc/tc_util.c

my %rates = (
    'bit'   => 1,
    'kibit' => 1024,
    'kbit'  => 1000.,
    'mibit' => 1048576.,
    'mbit'  => 1000000.,
    'gibit' => 1073741824.,
    'gbit'  => 1000000000.,
    'tibit' => 1099511627776.,
    'tbit'  => 1000000000000.,
    'bps'   => 8.,
    'kibps' => 8192.,
    'kbps'  => 8000.,
    'mibps' => 8388608.,
    'mbps'  => 8000000.,
    'gibps' => 8589934592.,
    'gbps'  => 8000000000.,
    'tibps' => 8796093022208.,
    'tbps'  => 8000000000000.,
);

# Rate can be something like "auto" or "10.2mbit"
sub getAutoRate {
    my ( $rate, $dev ) = @_;

    if ( $rate eq "auto" ) {
        $rate = interfaceRate($dev);
        if ( !defined $rate ) {
            print STDERR
              "Interface $dev speed cannot be determined (assuming 10mbit)\n";
            $rate = 10000000;
        }
    } else {
        $rate = getRate($rate);
    }

    return $rate;
}

sub getRate {
    my $rate = shift;
    defined $rate
      or die "Rate not defined";

    my ( $num, $suffix ) = get_num($rate);
    defined $num
      or die "$rate is not a valid bandwidth (not a number)\n";
    ( $num >= 0 )
      or die "$rate is not a valid bandwidth (negative value)\n";

    if ( defined $suffix ) {
        my $scale = $rates{ lc $suffix };

        if ( defined $scale ) {
            return $num * $scale;
        }

        die "$rate is not a valid bandwidth (unknown scale suffix)\n";
    } else {

        # No suffix implies Kbps just as IOS
        return $num * 1000;
    }
}

sub getPercent {
    my $percent = shift;
    my ( $num, $suffix ) = get_num($percent);

    if ( defined $suffix && $suffix ne '%' ) {
        die "$percent incorrect suffix (expect %)\n";
    } elsif ( !defined $num ) {
        die "$percent is not a valid percent (not a number)\n";
    } elsif ( $num < 0 ) {
        die "$percent is not a acceptable percent (negative value)\n";
    } elsif ( $num > 100 ) {
        die "$percent is not a acceptable percent (greater than 100%)\n";
    } else {
        return $num;
    }
}

# Default time units for tc are usec.
my %timeunits = (
    's'     => 1000000,
    'sec'   => 1000000,
    'secs'  => 1000000,
    'ms'    => 1000,
    'msec'  => 1000,
    'msecs' => 1000,
    'us'    => 1,
    'usec'  => 1,
    'usecs' => 1,
);

sub getTime {
    my $time = shift;
    my ( $num, $suffix ) = get_num($time);

    defined $num
      or die "$time is not a valid time interval (not a number)\n";
    ( $num >= 0 )
      or die "$time is not a valid time interval (negative value)\n";

    return $num * 1000 unless $suffix; # No suffix implies ms

    my $scale = $timeunits{ lc $suffix };
    die "$time is not a valid time interval (unknown suffix)\n"
	unless $scale;

    return $num * $scale;
}

my %scales = (
    'b'    => 1,
    'k'    => 1024,
    'kb'   => 1024,
    'kbit' => 1024 / 8,
    'm'    => 1024 * 1024,
    'mb'   => 1024 * 1024,
    'mbit' => 1024 * 1024 / 8,
    'g'    => 1024 * 1024 * 1024,
    'gb'   => 1024 * 1024 * 1024,
);

sub getBurstSize {
    my $size = shift;
    my ( $num, $suffix ) = get_num($size);

    defined $num
      or die "$size is not a valid burst size (not a number)\n";

    ( $num >= 0 )
      or die "$size is not a valid burst size (negative value)\n";

    return $num unless $suffix;

    my $scale = $scales{ lc $suffix };
    defined $scale
	or die "$size is not a valid burst size (unknown scale suffix)\n";

    return $num * $scale;
}

sub getProtocol {
    my ($str) = @_;

    defined $str or return;
    if ( $str =~ /^([0-9]+)|(0x[0-9a-fA-F]+)$/ ) {
        if ( $str <= 0 || $str > 255 ) {
            die "$str is not a valid protocol number\n";
        }
        return $str;
    }

    my ( $name, $aliases, $proto ) = getprotobyname($str);
    die "\"$str\" unknown protocol\n"
      unless $proto;
    die "$name is not usable as an IP protocol match\n"
      if ( $proto == 0 );

    return $proto;
}

# Parse /etc/iproute/rt_dsfield
# return a hex string "0x10" or undefined
sub getDsfield {
    my ($str)      = @_;
    my $match      = undef;
    my $dsFileName = '/etc/iproute2/rt_dsfield';

    defined $str or return;

    # match number (or hex)
    if ( $str =~ /^([0-9]+)|(0x[0-9a-fA-F]+)$/ ) {
        if ( $str < 0 || $str > 63 ) {
            die "$str is not a valid DSCP value\n";
        }

        # convert DSCP value to header value used by iproute
        return $str << 2;
    }

    open my $ds, '<', $dsFileName || die "Can't open $dsFileName, $!\n";
    while (<$ds>) {
        next if /^#/;
        chomp;
        my ( $value, $name ) = split;
        if ( $str eq $name ) {
            $match = $value;
            last;
        }
    }
    close($ds) or die "read $dsFileName error\n";

    ( defined $match ) or die "\"$str\" unknown DSCP value\n";
    return $match;
}

sub getIfIndex {
    my ($str) = @_;

    defined $str or return;
    open my $sysfs, "<",
      "/sys/class/net/$str/ifindex" || die "Unknown interface $str\n";
    my $ifindex = <$sysfs>;
    close($sysfs) or die "read sysfs error\n";
    chomp $ifindex;
    return $ifindex;
}

## interfaceRate("eth0")
# return result in bits per second
sub interfaceRate {
    my ($interface) = @_;
    my $speed;
    my $config = new Vyatta::Config;

    $config->setLevel("interfaces ethernet");
    if ( $config->exists("$interface") ) {
        $speed = $config->returnValue("$interface speed");
        if ( defined($speed) && $speed ne "auto" ) {
            return $speed * 1000000;
        }
    }

    # During boot it may take time for auto-negotiation
    for ( my $retries = 0 ; $retries < 5 ; $retries++ ) {
        $speed = ethtoolRate($interface);
        if ( defined $speed ) {
            last;
        }
        sleep 1;
    }

    return $speed;
}

## ethtoolRate("eth0")
# Fetch actual rate using ethtool and format to valid tc rate
sub ethtoolRate {
    my $dev  = shift;
    my $rate = undef;

    # Get rate of real device (ignore vlan)
    $dev =~ s/\.[0-9]+$//;

    open( my $ethtool, '-|', "/sbin/ethtool $dev 2>/dev/null" )
      or die "ethtool failed: $!\n";

    # ethtool produces:
    #
    # Settings for eth1:
    # Supported ports: [ TP ]
    # ...
    # Speed: 1000Mb/s
    while (<$ethtool>) {
        my @line = split;
        if ( $line[0] =~ /^Speed:/ ) {
            if ( $line[1] =~ /[0-9]+Mb\/s/ ) {
                $rate = $line[1];
                $rate =~ s#Mb/s#000000#;
            }
            last;
        }
    }
    close $ethtool;
    return $rate;
}

1;