# Author: Robert Bays <robert@vyatta.com>
# Date: 2010
# Description: interface between Vyatta templates and Quagga vtysh

# **** 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) 2010 Vyatta, Inc.
# All Rights Reserved.
# **** End License ****

package Vyatta::Quagga::Config;

use strict;
use warnings;

use lib "/opt/vyatta/share/perl5/";
use Vyatta::Config;

my $_DEBUG = 0;
my %_vtysh;
my %_vtyshdel;
my $_qcomref = '';
my $_vtyshexe = '/usr/bin/vtysh';

###  Public methods -
# Create the class.  
# input: $1 - level of the Vyatta config tree to start at
#        $2 - hash of hashes ref to Quagga set/delete command templates
sub new {
  my $that = shift;
  my $class = ref ($that) || $that;
  my $self = {
    _level  => shift,
    _qcref  => shift,
  };

  $_qcomref = $self->{_qcref};

  if (! _qtree($self->{_level}, 'del')) { return 0; }
  if (! _qtree($self->{_level}, 'set')) { return 0; }

  bless $self, $class;
  return $self;
}

# Set/check debug level 
# input: $1 - debug level
sub setDebugLevel {
  my ($self, $level) = @_;
  if ($level > 0) {
    $_DEBUG = $level; 
    return $level;
  }
  return 0;
}

sub returnDebugLevel {
  return $_DEBUG;
}

# reinitialize the vtysh hashes for troublshooting tree
# walk post object creation
sub _reInitialize {
  my ($self) = @_;

  %_vtysh = ();
  %_vtyshdel = ();
  _qtree($self->{_level}, 'del');
  _qtree($self->{_level}, 'set');
}

# populate an array reference with Quagga commands
sub returnQuaggaCommands {
  my ($self, $arrayref) = @_; 

  foreach my $key (reverse sort keys %_vtyshdel) {
    foreach my $string (@{$_vtyshdel{$key}}) {
      push @{$arrayref}, "$string";
    }
  }

  foreach my $key (sort keys %_vtysh) {
    foreach my $string (@{$_vtysh{$key}}) {
      push @{$arrayref}, "$string";
    }
  }

  return 1;
}

# methods to send the commands to Quagga
sub setConfigTree {
  my ($self, $level, $skip_list, $ordered_list) = @_;
  if (_setConfigTree($level, 0, 0, $skip_list, $ordered_list)) { return 1; }
  return 0;
}

sub setConfigTreeRecursive {
  my ($self, $level, $skip_list, $ordered_list) = @_;
  if (_setConfigTree($level, 0, 1, $skip_list, $ordered_list)) { return 1; }
  return 0;
}

sub deleteConfigTree {
  my ($self, $level, $skip_list, $ordered_list) = @_;
  if (_setConfigTree($level, 1, 0, $skip_list, $ordered_list)) { return 1; }
  return 0;
}

sub deleteConfigTreeRecursive {
  my ($self, $level, $skip_list, $ordered_list) = @_;
  if (_setConfigTree($level, 1, 1, $skip_list, $ordered_list)) { return 1; }
  return 0;
}

# Explicitly re-add an unchanged node to the list to be re-inserted into Quagga
# Note that this method is not recursive!
sub reInsertNode
{
	my ($self, $level) = @_;

	_pdebug(1, "_reAddNode: level: $level");
	if (! _qtree("$level", 'oneshot')) {
		return 0;
	}
}

### End Public methods -
### Private methods
sub _pdebug {
  my ($level, $msg) = @_;

  if (! defined $level) { return 0; }
  if ($_DEBUG >= $level) { print "DEBUG: $msg\n"; }
  return 1;
}

# traverse the set/delete trees and send commands to quagga
# set traverses from $level in tree top down.  
# delete traverses from bottom up in tree to $level.
# execute commands in tree one at a time.  If there is an error in vtysh,
# fail.  otherwise, remove command from tree on success as we may traverse
# this portion of the tree again otherwise.
# input: $1 - level of the tree to start at
#        $2 - delete bool
#        $3 - recursive bool
#        $4 - array of strings to skip 
#        $5 - array of strings to order the input to quagga
# output: none, return failure if needed
sub _setConfigTree {
  my ($level, $delete, $recurse, $skip, $ordered) = @_;
  my $qcom = $_qcomref;
  my @com_array = ();
  my @skip_list = ();
  my @ordered_list = ();

  if (defined $skip) { @skip_list = @$skip; }
  if (defined $ordered) { @ordered_list = @$ordered; }

  if ((! defined $level)   ||
      (! defined $delete)  ||
      (! defined $recurse))      { return 0; }

  # default tree is the set vtysh hash
  my $vtyshref = \%_vtysh;
  # default tree walk order is top down
  my $sortfunc = \&cmpf;

  # if this is delete, use delete vtysh hash and walk the tree bottom up
  if ($delete) { 
    $vtyshref = \%_vtyshdel; 
    $sortfunc = \&cmpb;
  }

  _pdebug(3, "_setConfigTree - enter - level: $level\tdelete: $delete\trecurse: $recurse\tskip: @skip_list\tordered_list: @ordered_list");

  # This loop walks the arrays of quagga commands and builds list to send to quagga
  foreach my $key (keys %$vtyshref) {
    _pdebug(3, "_setConfigTree - key $key");

    # skip parameters listed in skip_list
    my $found = 0;
    if ((scalar @skip_list) > 0) {
      foreach my $node (@skip_list) {
        if ($key =~ /$node/) { 
          $found = 1; 
          _pdebug(3, "_setConfigTree - key $node in skip list"); 
        }
      }
    }
    if ($found) { next; }

    # should we run the vtysh command with noerr?
    my $noerr = '';
    if ( (defined $qcom->{$key}->{'noerr'}) && (
         ($qcom->{$key}->{'noerr'} eq "both") || 
         (($qcom->{$key}->{'noerr'} eq "del") && ($delete)) ||
         (($qcom->{$key}->{'noerr'} eq "set") && (!$delete)))) { $noerr = 1; }

    # this conditional matches key to level exactly or if recurse, start of key to level
    if ((($recurse) && ($key =~ /^$level/)) || ((! $recurse) && ($key =~ /^$level$/))) {
      my $index = 0;
      foreach my $cmd (@{$vtyshref->{$key}}) {
        _pdebug(2, "_setConfigTree - key: $key \t cmd: $cmd");
        
        push @com_array, "$cmd  \036  $noerr";
        # remove this command so we don't hit it again in another Recurse call
        delete ${$vtyshref->{$key}}[$index];
        $index++;
      }
    }
  }

  # Now let's sort based on ordered_list
  my $index = 0;
  while (scalar @ordered_list > 0) {
    my $prio = shift @ordered_list;
    my $str = sprintf "%5d", $index;
    foreach my $line (@com_array) {
      # add sorting order meta-data to list
      $line =~ s/$prio/$str\:::$prio/;
    }
    $index++;
  }

  # and now send the commands to quagga
  foreach my $line (sort $sortfunc @com_array) {
    my ($order, $command, $noerr);

    # remove the ordered_list sorting meta-data
    $line =~ s/\s+\d+:::/ /;
    # split our commands into individual components
    ($order, $command, $noerr) = split /  \036  /, $line;
    if (! _sendQuaggaCommand("$command", "$noerr")) { return 0; }
  }

  return 1;
}

# sort subs for _setConfigTree
sub cmpf { $a cmp $b }
sub cmpb { $b cmp $a }

# properly format a Quagga command for vtysh and send to Quagga
# input: $1 - qVarReplaced Quagga Command string
#        $2 - boolean: should we use noerr?
# output: none, return failure if needed
sub _sendQuaggaCommand {
  my ($command, $noerr) = @_;
  
  my @arg_array = ("$_vtyshexe");
  if ($noerr) { push (@arg_array, '--noerr'); }
  if (returnDebugLevel() >= 2) { push (@arg_array, '-E'); }
  push (@arg_array, '-c');
  push (@arg_array, 'configure terminal');

  my @commands = split / ; /, $command;
  foreach my $section (@commands) {
    push (@arg_array, '-c');
    push (@arg_array, "$section");
  }
  
  system(@arg_array) == 0 or _logger(\@arg_array, 1);

  return 1;
}

# log error message to syslog, optionally die
# input: $1 - reference to error message array
#        $2 - die boolean
sub _logger {
  my $error_array = shift;
  my $die = shift;
  my @logger_cmd = ("/usr/bin/logger");

  push (@logger_cmd, "-i");
  push (@logger_cmd, "-t vyatta-cfg-quagga");
  if (returnDebugLevel() > 0) { push (@logger_cmd, "-s"); }
  push (@logger_cmd, "@{$error_array} failed: $?");

  system(@logger_cmd) == 0 or die "unable to log system error message.";

  if ($die) { die "Error configuring routing subsystem.  See log for more detailed information\n"; }
  return 1;
}

# translate a Vyatta config tree into a Quagga command using %qcom as a template.
# input: $1 - Vyatta config tree string
#        $2 - Quagga command template string
# output: Quagga command suitable for vtysh as defined by %qcom.
sub _qVarReplace {
  my $node = shift;
  my $qcommand = shift;

  _pdebug(2, "_qVarReplace entry: node - $node");
  _pdebug(2, "_qVarReplace entry: qcommand - $qcommand");

  my @nodes = split /\s/, $node;
  my @qcommands = split /\s/, $qcommand;

  my $result = '';
  # try to replace (#num, ?var) references foreach item in Quagga command template array
  # with their corresponding value in Vyatta command array at (#num) index
  foreach my $token (@qcommands) {
    # is this a #var reference? if so translate and append to result
    if ($token =~ s/\#(\d+);*/$1/) {
      $token--;
      $result="$result $nodes[$token]";
    }
    # is this a ?var reference? if so check for existance of the var in Vyatta Config 
    # tree and conditionally append.  append token + value.  These conditional vars 
    # will only work at EOL in template string.
    elsif ($token =~ s/\?(\w+);*/$1/) {
      # TODO: Vyatta::Config needs to be fixed to accept level in constructor
      my $config = new Vyatta::Config;
      $config->setLevel($node);
      my $value = $config->returnValue($token);
      if ($value) { $result = "$result $token $value"; }
      elsif ($config->exists($token)) { $result = "$result $token"; }
    }
    # is this a @var reference? if so, append just the value instead of token + value
    elsif ($token =~ s/\@(\w+);*/$1/) {
      my $config = new Vyatta::Config;
      $config->setLevel($node);
      my $value = $config->returnValue($token);
      if (defined $value) { $result = "$result $value"; }
    }
    # if not, just append string to result
    else {
      $result = "$result $token";
    }
  }

  # remove leading space characters
  $result =~ s/^\s(.+)/$1/;
  _pdebug(2, "_qVarReplace exit: result - $result");

  return $result;
}

# For given Vyatta config tree string, find a corresponding Quagga command template 
# string as defined in %qcom
# input: $1 - Vyatta config tree string
#        $2 - action (set|del)
#        $3 - Quagga command template hash
# output: %qcom hash key to corresponding Quagga command template string
sub _qCommandFind {
  my $vyattaconfig = shift;
  my $action = shift;
  my $qcom = shift;
  my $command = '';

  my @nodes = split /\s+/, $vyattaconfig;

  # append each token in the Vyatta config tree.  sequentially  
  # check if there is a corresponding hash in %qcom.  if not,
  # do same check again replacing the end param with var to see
  # if this is a var replacement
  foreach my $token (@nodes) {
    if    (exists $qcom->{$token}->{$action})            { $command = $token; }
    elsif (exists $qcom->{"$command $token"}->{$action}) { $command = "$command $token"; }
    elsif (exists $qcom->{"$command var"}->{$action})    { $command = "$command var"; }
    else { return; }
  }

  # return hash key if Quagga command template string is found
  if (defined $qcom->{$command}->{$action}) { return $command; }
  else { return; }
}

# translate the adds/changes in a Vyatta config tree into Quagga vtysh commands.
# recursively walks the tree.  
# input:  $1 - the level of the Vyatta config tree to start at
#			   ex: protocols bgp 1 neighbor 1.1.1.1 remote-as 10
#         $2 - the action (set|delete|oneshot)
# output: none - creates the %vtysh that contains the Quagga add commands
sub _qtree {
  my ($level, $action) = @_;
  my @nodes;
  my $oneshot = 0;
  my $qcom = $_qcomref;
  my $vtysh = \%_vtysh;
  my $config = new Vyatta::Config;
  $config->setLevel($level);
  
  # setup data structures and references for various action types
  if ($action eq 'oneshot') {
	# split the last parameter out of the level string
	$level =~ s/\s+/ /g;
	my $index = rindex($level, " ");
	my $node = substr($level, $index + 1);
	$level = substr($level, 0, (-1 * length($node)));
	chop $level;
	$config->setLevel($level);
	push @nodes, $node;
	$action = 'set';
	$oneshot = 1;
  } elsif ($action eq 'set') {
    @nodes = $config->listNodes();
  } else {
    $vtysh = \%_vtyshdel;
    @nodes = $config->listDeleted();

    # handle special case for multi-nodes values being deleted
    # listDeleted() doesn't return the node as being deleted if 
    # it is a multi unless all values are deleted.  This is suboptimal.
    my @all_nodes = $config->listNodes();
    foreach my $node (@all_nodes) {
      my @array = split /\s+/, $level;
      push @array, $node;
      my ($multi, $text, $default) = $config->parseTmpl(\@array);
      if ($multi) {
        my @orig_values = $config->returnOrigValues("$node");
        my @new_values = $config->returnValues("$node");
        my %chash = $config->compareValueLists(\@orig_values, \@new_values);
        if (${$chash{'deleted'}}[0]) {                 
          push @nodes, $node;
        }
      }
    }
  } ## end else {

  _pdebug(1, "_qtree - action: $action\tlevel: $level");

  # traverse the Vyatta config tree and translate to Quagga commands where apropos
  if (@nodes > 0) {
    foreach my $node (@nodes) {
      _pdebug(2, "_qtree - foreach node loop - node $node");

      # for set action, need to check that the node was actually changed.  Otherwise
      # we end up re-writing every node to Quagga every commit.  For the other action
      # types, just do it. 
      if ($config->isChanged("$node") || ($action eq 'del') || $oneshot) {
        # is there a Quagga command template?
        # TODO: need to add function reference support to qcom hash for complicated nodes
        my $qcommand = _qCommandFind("$level $node", $action, $qcom);

        # if I found a Quagga command template, then replace any vars
        if ($qcommand) {
          # get the apropos config value so we can use it in the Quagga command template
          my @vals = undef;

          # This is either a set or delete on a single or multi: node
          if ($action eq 'set') {
            my $tmplhash = $config->parseTmplAll($node);
            if ($tmplhash->{'multi'}) {
              _pdebug(2, "multi");
              @vals = $config->returnValues($node);
            }
            else {
              _pdebug(2, "not a multi");
              $vals[0] = $config->returnValue($node);
            }
          }
          else {
            my $tmplhash = $config->parseTmplAll($node);
            if ($tmplhash->{'multi'}) {
              _pdebug(2, "multi"); 
              @vals = $config->returnOrigValues($node);
            }
            else {
              _pdebug(2, "not a multi");
              $vals[0] = $config->returnOrigValue($node);
            }
          }

          # is this a leaf node?
          if (defined $vals[0]) {
            foreach my $val (@vals) {
              my $var = _qVarReplace("$level $node $val", $qcom->{$qcommand}->{$action});
              push @{$vtysh->{"$qcommand"}}, "$level $node $val  \036  $var";
              _pdebug(1, "_qtree leaf node command: $action $level $node $val \n\t\t\t\t\t$var");
            }
          } else {
            my $var = _qVarReplace("$level $node", $qcom->{$qcommand}->{$action});
            push @{$vtysh->{"$qcommand"}}, "$level $node  \036  $var";
            _pdebug(1, "_qtree node command: $action $level $node \n\t\t\t\t$var");
          }
        }
      }
      
      # recurse to next level in tree
      if (! $oneshot) {
      	_qtree("$level $node", 'del');
      	_qtree("$level $node", 'set');
 	  }
    }
  }

  return 1;
}

return 1;
