#! /usr/bin/perl
#
# Module: vyatta-update-arp-params
#
# **** License ****
# Copyright (c) 2019-2020, AT&T Intellectual Property. All rights reserved.
# Copyright (c) 2014-2016 by Brocade Communications Systems, Inc.
# All rights reserved.
#
# This code was originally developed by Vyatta, Inc.
# Portions created by Vyatta are Copyright (C) 2009 Vyatta, Inc.
# All Rights Reserved.
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Author: Mohit Mehta
# Date: February 2009
# Description: Update [ARP (IPv4)|Neighbor (IPV6)] Parameters
#
# **** End License ****
#

use strict;
use warnings;
use lib "/opt/vyatta/share/perl5";
use File::Slurp qw( write_file );
use Getopt::Long;
use Vyatta::Configd;
use Vyatta::VPlaned;
use vyatta::proto::NbrResConfig;

# ARP
my @arp_params       = ( "stale-time", "table-size" );
my $arp_node         = "arp";
my $arp_path         = "system ip $arp_node";
my $arp_prot         = "arp";
my $arp_def_tbl_size = 1024;

# ND
my @nd_params       = ( "resolution-throttling", "table-size" );
my $nd_node         = "neighbor";
my $nd_path         = "system ipv6 $nd_node";
my $nd_prot         = "nd6";
my $nd_def_tbl_size = 8192;

# BOTH
my %dp_actions = (
    'SET'    => NbrResConfig::Action::SET(),
    'DELETE' => NbrResConfig::Action::DELETE()
);
my %dp_params = (
    'stale-time'            => NbrResConfig::Param::AGING_TIME(),
    'resolution-throttling' => NbrResConfig::Param::RES_TOKEN(),
    'table-size'            => NbrResConfig::Param::MAX_ENTRY()
);
my %dp_prots = (
    'arp' => NbrResConfig::Prot::ARP(),
    'nd6' => NbrResConfig::Prot::ND6()
);
my $def_stale_time = 60;
my $min_tbl_size   = 1;

sub set_dp_param {
    my ( $prot, $action, $param, $value, $dev ) = @_;
    my $cstore = new Vyatta::VPlaned;
    my $dp_param = $dp_params{$param};

    $dev = "all" unless $dev;
    $action = "DELETE" unless defined($value);

    my $msg = NbrResConfig->new(
        {
            prot   => $dp_prots{$prot},
            action => $dp_actions{$action},
            ifname => $dev,
            param  => $dp_param,
            value  => $value,
        }
    );
    $cstore->store_pb(
        "$prot $dev $dp_param",
        $msg, "vyatta:" . $prot,
        $dev, $action
    );
    return;
}

sub set_kernel_gc_thresholds {
    use integer;
    my ( $ip, $max, $min_in ) = @_;
    my $soft_max = $max / 2;
    my $min = defined($min_in) ? $min_in : $max / 8;

    system("sysctl -q net.$ip.neigh.default.gc_thresh3=$max");
    system("sysctl -q net.$ip.neigh.default.gc_thresh2=$soft_max");
    system("sysctl -q net.$ip.neigh.default.gc_thresh1=$min");
    return;
}

# The gc_stale_time kernel parameter has a 'default' but no 'all' target, so
# in addition to "sysctl -q net.$ip.neigh.default.gc_stale_time=$time", need
# to set the parameter for some interface types. As system calls and retrieving
# interfaces from config is expensive, update this parameter directly for all
# interfaces.
sub set_kernel_gc_stale_time {
    my ( $ip, $time ) = @_;
    my $dir   = "/proc/sys/net/$ip/neigh";
    my $param = "gc_stale_time";
    my $err_i;

    opendir( DIR, $dir ) or die "Could not open $dir: $!";
    while ( my $i = readdir DIR ) {
        next if !index( $i, '.' );
        write_file( "$dir/$i/$param", { err_mode => 'quiet' }, $time )
          or $err_i = $i;
    }
    closedir DIR;
    print "Warning: unable to set $param for some interfaces, e.g. $err_i\n"
      if $err_i;
    return;
}

sub usage {
    print "Usage: $0 --action={SET|ACTIVE|DELETE} [--param=<param-name>] ",
      "[--value=<value>] [--dev=<interface>] [--ipv6]\n";
    exit 1;
}

my ( $action, $ipv6, $param_in, $value_in, $dev );

GetOptions(
    "action=s" => \$action,
    "param=s"  => \$param_in,
    "value=s"  => \$value_in,
    "dev=s"    => \$dev,
    "ipv6"     => \$ipv6,
) or usage();

$action = $ENV{COMMIT_ACTION} unless $action;
usage() unless $action && grep { $_ eq $action } ( "SET", "ACTIVE", "DELETE" );
$action = "SET" unless $action eq "DELETE";

my ( $ip, $node, $path, $prot, $def_tbl_size, @params );

if ($ipv6) {
    ( $ip, $node, $path, $prot, $def_tbl_size, @params ) =
      ( "ipv6", $nd_node, $nd_path, $nd_prot, $nd_def_tbl_size, @nd_params );
} else {
    ( $ip, $node, $path, $prot, $def_tbl_size, @params ) = (
        "ipv4", $arp_node, $arp_path, $arp_prot, $arp_def_tbl_size, @arp_params
    );
}

my ( $config, $value );

if ( defined($param_in) ) {
    usage() unless grep { $_ eq $param_in } @params;
    usage() if !defined($value_in) && $action eq "SET";
    @params = ($param_in);
} else {
    my $client = Vyatta::Configd::Client->new();
    $config = $client->tree_get_hash($path)
      if $client->node_exists( $Vyatta::Configd::Client::AUTO, $path );
}

for my $param (@params) {

    # Resolution throttling is only available in the dataplane
    if ( $param eq "resolution-throttling" ) {
        $value = $config ? $config->{$node}->{$param} : $value_in;
        set_dp_param( $prot, $action, $param, $value, $dev );
    }

    # Stale time applies to both dataplane and kernel
    elsif ( $param eq "stale-time" ) {
        $value = $config ? $config->{$node}->{$param} : $value_in;
        set_dp_param( $prot, $action, $param, $value, $dev );

        $value = $def_stale_time unless defined($value) && $action eq "SET";
        set_kernel_gc_stale_time( $ip, $value );
    }

    # Table size applies to both dataplane and kernel
    elsif ( $param eq "table-size" ) {
        $value = $config ? $config->{$node}->{$param} : $value_in;
        set_dp_param( $prot, $action, $param, $value, $dev );

        my $min;
        if ( !defined($value) || $action eq "DELETE" ) {
            $value = $def_tbl_size;

            # If stale time is set and table size is not, then configure kernel
            # cache so gc runs if there any entries. This ensures that the stale
            # timeout is enforced regardless of number of entries in cache, for
            # test purposes. This is not suitable at any scale due to memory
            # thrashing, so is not done if a table size is configured.
            $min = $min_tbl_size
              if $config && $config->{$node}->{'stale-time'};
        }
        set_kernel_gc_thresholds( $ip, $value, $min );
    }
}
