#!/usr/bin/perl
#
# Module: vyatta-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.
#
# A copy of the GNU General Public License is available as
# `/usr/share/common-licenses/GPL' in the Debian GNU/Linux distribution
# or on the World Wide Web at `http://www.gnu.org/copyleft/gpl.html'.
# You can also obtain it by writing to the Free Software Foundation,
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
# 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: November 2007
# Description: Script to assign addresses to interfaces.
#
# **** End License ****
#

use lib "/opt/vyatta/share/perl5/";
use Vyatta::Config;
use Vyatta::Misc qw(generate_dhclient_intf_files
  getInterfaces getIP get_sysfs_value
  is_address_enabled is_dhcp_enabled is_ip_v4_or_v6);
use Vyatta::File qw(touch);
use Vyatta::Interface;

use Getopt::Long;

use strict;
use warnings;

my $dhcp_daemon = '/sbin/dhclient';
my $ETHTOOL   = '/sbin/ethtool';

my ($dev, $mac, $mac_update);
my %skip_interface;
my ($check_name, $show_names, $vif_name, $warn_name);
my ($check_up, $dhcp_command, $allowed_speed);
my (@speed_duplex, @addr_commit, @check_speed, @offload_option);

sub usage {
  print <<EOF;
Usage: $0 --dev=<interface> --check=<type>
     $0 --dev=<interface> --warn
     $0 --dev=<interface> --valid-mac=<aa:aa:aa:aa:aa:aa>
     $0 --dev=<interface> --valid-addr-commit={addr1 addr2 ...}
     $0 --dev=<interface> --speed-duplex=speed,duplex
     $0 --dev=<interface> --check-speed=speed,duplex
     $0 --dev=<interface> --allowed-speed
     $0 --dev=<interface> --isup
     $0 --dev=<interface> --offload-option={tcp-segmention,udp-fragmentation} {value}
     $0 --dev=<interface> --offload-option={generic-segmentation,generic-receive} {value}
     $0 --dev=<interface> --offload-option={scatter-gather} {value}
     $0 --show=<type>
EOF
  exit 1;
}

GetOptions(
  "valid-addr-commit=s{,}" => \@addr_commit,
  "dev=s"       => \$dev,
  "valid-mac=s"     => \$mac,
  "set-mac=s"     => \$mac_update,
  "dhcp=s"      => \$dhcp_command,
  "check=s"       => \$check_name,
  "show=s"      => \$show_names,
  "skip=s"      => sub {$skip_interface{$_[1]} = 1},
  "vif=s"       => \$vif_name,
  "warn"        => \$warn_name,
  "isup"        => \$check_up,
  "speed-duplex=s{2}" => \@speed_duplex,
  "check-speed=s{2}"  => \@check_speed,
  "allowed-speed"   => \$allowed_speed,
  "offload-option=s{2}" => \@offload_option,
) or usage();

is_valid_addr_commit($dev, @addr_commit)  if (@addr_commit);
is_valid_mac($mac, $dev)          if ($mac);
update_mac($mac_update, $dev)       if ($mac_update);
dhcp($dhcp_command, $dev)         if ($dhcp_command);
is_valid_name($check_name, $dev)      if ($check_name);
exists_name($dev)             if ($warn_name);
show_interfaces($show_names)        if ($show_names);
is_up($dev)                 if ($check_up);
set_speed_duplex($dev, @speed_duplex)   if (@speed_duplex);
check_speed_duplex($dev, @check_speed)  if (@check_speed);
allowed_speed($dev)             if ($allowed_speed);
set_offload_option($dev, @offload_option) if (@offload_option);
exit 0;

sub is_ip_configured {
  my ($intf, $ip) = @_;
  my $found = grep {$_ eq $ip} Vyatta::Misc::getIP($intf);
  return ($found > 0);
}

sub is_ipv4 {
  return index($_[0],':') < 0;
}

sub is_up {
  my $name = shift;
  my $intf = new Vyatta::Interface($name);

  die "Unknown interface type for $name" unless $intf;

  exit 0 if ($intf->up());
  exit 1;
}

sub dhcp_write_file {
  my ($file, $data) = @_;

  open(my $fh, '>', $file) || die "Couldn't open $file - $!";
  print $fh $data;
  close $fh;
}

sub dhcp_conf_header {
  my $output;

  my $date = `date`;
  chomp $date;
  $output  = "#\n# autogenerated by vyatta-interfaces.pl on $date\n#\n";
  return $output;
}

sub get_hostname {
  my $config = new Vyatta::Config;
  $config->setLevel("system");
  return $config->returnValue("host-name");
}

sub is_domain_name_set {
  my $config = new Vyatta::Config;
  $config->setLevel("system");
  return $config->returnValue("domain-name");
}

sub get_mtu {
  my $name = shift;
  my $intf = new Vyatta::Interface($name);
  return $intf->mtu();
}

sub get_dhcp_client_id {
  my $name = shift;
  my $intf = new Vyatta::Interface($name);
  my $config = new Vyatta::Config;
  $config->setLevel($intf->path());
  return $config->returnValue("dhcp-options client-id");
}

sub get_dhcp_hostname {
  my $name = shift;
  my $intf = new Vyatta::Interface($name);
  my $config = new Vyatta::Config;
  $config->setLevel($intf->path());
  return $config->returnValue("dhcp-options host-name");
}

sub dhcp_update_config {
  my ($conf_file, $intf) = @_;

  my $output = dhcp_conf_header();
  my $hostname = get_dhcp_hostname($intf);
  if (!defined($hostname)) {
    $hostname = get_hostname();
  }

  $output .= "option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;\n";

  $output .= "interface \"$intf\" {\n";
  if (defined($hostname)) {
    $output .= "\tsend host-name \"$hostname\";\n";
  }

  my $client_id = get_dhcp_client_id($intf);
  if (defined($client_id)) {
    $output .= "\tsend dhcp-client-identifier \"$client_id\";\n";
  }

  $output .= "\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes";
  my $domainname = is_domain_name_set();
  if (!defined($domainname)) {
    $output .= ", domain-name";
  }

  my $mtu = get_mtu($intf);
  $output .= ", interface-mtu" unless $mtu;

  $output .= ";\n";
  $output .= "}\n\n";

  dhcp_write_file($conf_file, $output);
}

# Is interface disabled in configuration (only valid in config mode)
sub is_intf_disabled {
  my $name = shift;
  my $intf = new Vyatta::Interface($name);
  $intf or die "Unknown interface name/type: $name\n";

  my $config = new Vyatta::Config;
  $config->setLevel($intf->path());

  return $config->exists("disable");
}

sub run_dhclient {
  my ($intf, $op_mode) = @_;

  my ($intf_config_file, $intf_process_id_file, $intf_leases_file)= generate_dhclient_intf_files($intf);

  # perform config mode actions if not called from op-mode
  if (!defined $op_mode) {
    dhcp_update_config($intf_config_file, $intf);
    return if is_intf_disabled($intf);
  }

  my $cmd = "$dhcp_daemon -pf $intf_process_id_file -x $intf 2> /dev/null; rm -f $intf_process_id_file 2> /dev/null;";
  $cmd .= "$dhcp_daemon -q -nw -cf $intf_config_file -pf $intf_process_id_file  -lf $intf_leases_file $intf 2> /dev/null &";

  # adding & at the end to make the process into a daemon immediately
  system($cmd) == 0
    or warn "start $dhcp_daemon failed: $?\n";
}

sub stop_dhclient {
  my ($intf, $op_mode) = @_;

  # perform config mode actions if not called from op-mode
  if (!defined $op_mode) {
    return if is_intf_disabled($intf);
  }

  my ($intf_config_file, $intf_process_id_file, $intf_leases_file)= generate_dhclient_intf_files($intf);
  my $release_cmd = "$dhcp_daemon -q -cf $intf_config_file -pf $intf_process_id_file -lf $intf_leases_file -r $intf 2> /dev/null;";
  $release_cmd .= "rm -f $intf_process_id_file 2> /dev/null";
  system($release_cmd) == 0
    or warn "stop $dhcp_daemon failed: $?\n";
}

sub update_mac {
  my ($mac, $name) = @_;
  my $intf = new Vyatta::Interface($name);
  $intf or die "Unknown interface name/type: $name\n";

  # maybe nothing needs to change
  my $oldmac = $intf->hw_address();
  exit 0 if (lc($oldmac) eq lc($mac));

  # try the direct approach
  if (system("sudo ip link set $name address $mac") == 0) {
    exit 0;
  } elsif ($intf->up()) {

    # some hardware can not change MAC address if up
    system "sudo ip link set $name down"
      and die "Could not set $name down\n";
    system "sudo ip link set $name address $mac"
      and die "Could not set $name address\n";
    system "sudo ip link set $name up"
      and die "Could not set $name up\n";
  } else {
    die "Could not set mac address for $name\n";
  }

  exit 0;
}

sub is_vrrp_mac {
  my @octets = @_;
  return 1 if (   hex($octets[0]) == 0
         && hex($octets[1]) == 0
         && hex($octets[2]) == 94
         && hex($octets[3]) == 0
         && hex($octets[4]) == 1);
  return 0;
}

sub is_valid_mac {
  my ($mac, $intf) = @_;
  my @octets = split /:/, $mac;

  ($#octets == 5) or die "Error: wrong number of octets: $#octets\n";

  ((hex($octets[0]) & 1) == 0) or die "Error: $mac is a multicast address\n";

  is_vrrp_mac(@octets) and die "Error: $mac is a vrrp mac address\n";

  my $sum = 0;
  $sum += hex($_) foreach @octets;
  ($sum != 0) or die "Error: zero is not a valid address\n";

  exit 0;
}

# Validate the set of address values configured on an interface at commit
# Check that full set of address address values are consistent.
#  1. Interface may not be part of bridge or bonding group
#  2. Can not have both DHCP and a static IPv4 address.
sub is_valid_addr_commit {
  my ($ifname, @addrs) = @_;
  my $intf = new Vyatta::Interface($ifname);
  $intf or die "Unknown interface name/type: $ifname\n";

  my $config = new Vyatta::Config;
  $config->setLevel($intf->path());

  my $bridge = $config->returnValue("bridge-group bridge");
  die "Can't configure address on interface that is port of bridge.\n"
    if (defined($bridge));

  my $bond = $config->returnValue("bond-group");
  die "Can't configure address on interface that is slaved to bonding interface.\n"
    if (defined($bond));

  my $addrmap = Vyatta::Interface::get_cfg_addresses();

  my ($dhcp, $static_v4);
  foreach my $addr (@addrs) {
    next if ($addr eq 'dhcpv6');

    if ($addr eq 'dhcp') {
      $dhcp = 1;
      next;
    }

    $static_v4 = 1
      if (is_ipv4($addr));
  }

  die "Can't configure static IPv4 address and DHCP on the same interface.\n"
    if ($static_v4 && $dhcp);

  exit 0;
}

# Is interface currently in admin down state?
sub is_intf_down {
  my $name = shift;
  my $intf = new Vyatta::Interface($name);

  return 1 unless $intf;
  return !$intf->up();
}

sub dhcp {
  my ($request, $intf) = @_;

  die "$intf is not using DHCP to get an IP address\n"
    unless ($request eq 'start' || is_dhcp_enabled($intf));

  die "$intf is disabled.\n"
    if ($request ne 'stop' && is_intf_down($intf));

  my $tmp_dhclient_dir = '/var/run/vyatta/dhclient/';
  my $release_file = $tmp_dhclient_dir . 'dhclient_release_' . $intf;
  if ($request eq "release") {
    die "IP address for $intf has already been released.\n"
      if (-e $release_file);

    print "Releasing DHCP lease on $intf ...\n";
    stop_dhclient($intf, 'op_mode');
    mkdir($tmp_dhclient_dir) if (!-d $tmp_dhclient_dir);
    touch($release_file);
  } elsif ($request eq "renew") {
    print "Renewing DHCP lease on $intf ...\n";
    run_dhclient($intf, 'op_mode');
    unlink($release_file);
  } elsif ($request eq "start") {
    print "Starting DHCP client on $intf ...\n";
    touch("/var/lib/dhcp/$intf");
    run_dhclient($intf);
  } elsif ($request eq "stop") {
    print "Stopping DHCP client on $intf ...\n";
    stop_dhclient($intf);
    unlink("/var/lib/dhcp/dhclient_$intf\_lease");
    unlink("/var/lib/dhcp/$intf");
    unlink("/var/run/vyatta/dhclient/dhclient_release_$intf");
    unlink("/var/lib/dhcp/dhclient_$intf\.conf");
  } else {
    die "Unknown DHCP request: $request\n";
  }

  exit 0;
}

sub is_valid_name {
  my ($type, $name) = @_;
  die "Missing --dev argument\n" unless $name;

  my $intf = new Vyatta::Interface($name);
  die "$name does not match any known interface name type\n"
    unless $intf;

  my $vif = $intf->vif();
  die "$name is the name of VIF interface\n","Need to use \"interface ",$intf->physicalDevice()," vif $vif\"\n"
    if $vif;

  die "$name is a ", $intf->type(), " interface not an $type interface\n"
    if ($type ne 'all' and $intf->type() ne $type);

  die "$type interface $name does not exist on system\n"
    unless grep {$name eq $_} getInterfaces();

  exit 0;
}

sub exists_name {
  my $name = shift;
  die "Missing --dev argument\n" unless $name;

  warn "interface $name does not exist on system\n"
    unless grep {$name eq $_} getInterfaces();
  exit 0;
}

# generate one line with all known interfaces (for allowed)
sub show_interfaces {
  my $type = shift;
  my @interfaces = getInterfaces();
  my @match;

  foreach my $name (@interfaces) {
    my $intf = new Vyatta::Interface($name);
    next unless $intf;		# skip unknown types
    next if $skip_interface{$name};
    next unless ($type eq 'all' || $type eq $intf->type());
    if ($intf->vrid()){
      push @match, $name; # add all vrrp interfaces
    } elsif ($vif_name) {
      next unless $intf->vif();
      push @match, $intf->vif()
        if ($vif_name eq $intf->physicalDevice());
    } else {
      push @match, $name
        unless $intf->vif()
        and $type ne 'all';
    }
  }
  print join(' ', @match), "\n";
}

# Determine current values for autoneg, speed, duplex
sub get_ethtool {
  my $dev = shift;

  open(my $ethtool, '-|', "$ETHTOOL $dev 2>&1")
    or die "ethtool failed: $!\n";

  # ethtool produces:
  #
  # Settings for eth1:
  # Supported ports: [ TP ]
  # ...
  # Speed: 1000Mb/s
  # Duplex: Full
  # ...
  # Auto-negotiation: on
  my ($rate, $duplex) = (1000, "full");
  my $autoneg = 0;
  while (<$ethtool>) {
    chomp;
    return if (/^Cannot get device settings/);

    if (/^\s+Speed: (\d+)Mb/) {
      $rate = $1;
    } elsif (/^\s+Duplex:\s(.*)$/) {
      $duplex = lc $1;
    } elsif (/^\s+Auto-negotiation: on/) {
      $autoneg = 1;
    }
  }
  close $ethtool;

  $duplex = "full" if $duplex =~ /unknown.*/; 

  return ($autoneg, $rate, $duplex);
}

sub set_speed_duplex {
  my ($intf, $nspeed, $nduplex) = @_;
  die "Missing --dev argument\n" unless $intf;

  ## if driver virtio, speed and duplex are unknown per default coming fromthe driver itself
  ## if that's the case we always run ethtool and set the values

  open(my $ethtool, '-|', "$ETHTOOL -i $dev 2>&1")
    or die "ethtool failed: $!\n"; 
  my $drv = 0;
  while (<$ethtool>)
  {
    chomp;
    return if (/^Cannot get device driver settings/);
    $drv = 1 if (/^driver:.*/); 
    last;
  }

  if ($drv != 1)
  {
    # read old values to avoid meaningless speed changes
    my ($autoneg, $ospeed, $oduplex) = get_ethtool($intf);

    if (defined($autoneg) && $autoneg == 1) {

      # Device is already in autonegotiation mode
      return if ($nspeed eq 'auto');
    } elsif (defined($ospeed) && defined($oduplex)) {

      # Device has explicit speed/duplex but they already match
      return if (($nspeed eq $ospeed) && ($nduplex eq $oduplex));
    }
  }

  my $cmd = "$ETHTOOL -s $intf";
  if ($nspeed eq 'auto') {
    $cmd .= " autoneg on";
  } else {
    $cmd .= " speed $nspeed duplex $nduplex autoneg off";
  }

  exec $cmd;
  die "exec of $ETHTOOL failed: $!";
}

# Check if speed and duplex value is supported by device
sub is_supported_speed {
  my ($dev, $speed, $duplex) = @_;

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

  ### check if virtualized NIC
  return 1 if <$if_drv> =~ /virtio_net/;

  my $wanted = sprintf("%dbase%s/%s", $speed,($speed == 2500) ? 'X' : 'T', ucfirst($duplex));

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

  # ethtool output:
  #
  # Settings for eth1:
  #	Supported ports: [ TP ]
  #	Supported link modes:   10baseT/Half 10baseT/Full
  #	            100baseT/Half 100baseT/Full
  #	            1000baseT/Half 1000baseT/Full
  #   Supports auto-negotiation: Yes
  my $mode;
  while (<$ethtool>) {
    chomp;
    if ($mode) {
      last unless /^\t /;
    } else {
      next unless /^\tSupported link modes: /;
      $mode = 1;
    }

    return 1 if /$wanted/;
  }

  close $ethtool;

  return;
}

# Validate speed and duplex settings prior to commit
sub check_speed_duplex {
  my ($dev, $speed, $duplex) = @_;

  # most basic and default case
  exit 0 if ($speed eq 'auto' && $duplex eq 'auto');

  die "If speed is hardcoded, duplex must also be hardcoded\n"
    if ($duplex eq 'auto');

  die "If duplex is hardcoded, speed must also be hardcoded\n"
    if ($speed eq 'auto');

  die "Speed $speed, duplex $duplex not supported on $dev\n"
    unless is_supported_speed($dev, $speed, $duplex);

  exit 0;
}

# Produce list of valid speed values for device
sub allowed_speed {
  my ($dev) = @_;

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

  my %speeds;
  my $first = 1;
  while (<$ethtool>) {
    chomp;

    if ($first) {
      next unless s/\tSupported link modes:\s//;
      $first = 0;
    } else {
      last unless /^\t /;
    }

    foreach my $val (split / /) {
      $speeds{$1} = 1 if $val =~ /(\d+)base/;
    }
  }

  close $ethtool;
  print 'auto ', join(' ', sort keys %speeds), "\n";
}

sub get_offload_option {
  my ($dev, $option) = @_;
  my $val;
  my $ethtool_option;

  if ($option ne 'scatter-gather') {
    $ethtool_option = "$option-offload";
  } else {
    $ethtool_option = $option;
  }

  open(my $ethtool, '-|', "$ETHTOOL -k $dev 2>&1") or die "ethtool failed: $!\n";
  while (<$ethtool>) {
    next if ($_ !~ m/$ethtool_option:/);
    chomp;
    $val = (split(/: /, $_))[1];
  }
  close $ethtool;
  return $val;

}

sub set_offload_option {
  my ($intf, $option, $nvalue) = @_;
  die "Missing --dev argument\n" unless $intf;

  my $ovalue = get_offload_option($intf, $option);

  my %ethtool_opts = (
    'generic-receive'    => 'gro',
    'generic-segmentation' => 'gso',
    'tcp-segmentation'   => 'tso',
    'udp-fragmentation'  => 'ufo',
    'scatter-gather'     => 'sg',
  );

  if (defined($nvalue) && $nvalue ne $ovalue) {
    my $cmd = "$ETHTOOL -K $intf $ethtool_opts{$option} $nvalue";

    system($cmd);
    if ($? >> 8) {
      die "Offload option for $option is not supported on $intf\n";
    }
  }

}

