#!/usr/bin/perl

use strict;               # always.
use warnings;
no  warnings 'uninitialized';
use English;              

use Getopt::Long;
use Pod::Usage;

use Sys::Syslog;               # we need to syslog
use Time::HiRes qw( gettimeofday tv_interval sleep);

use POSIX ":sys_wait_h";
use POSIX qw(setsid dup2);     # we are proper daemons, we are.

use Config::General;      # nice apache-style config
use Storable qw(freeze nfreeze);

use IO::Socket::UNIX;
use IO::Select;
use IO::Pipe;

use Data::Dumper;

# use Data::Dumper;

use constant DO_THREADS  => 1;

use constant OK          => 0;
use constant RECOVERED   => 1;
use constant DEAD        => 2;
use constant INACTIVE    => 3;


use constant CVS_ID   => '$Id: lvs-kiss,v 1.80 2003/06/20 17:47:11 perbu Exp $';

use constant DEFAULTWEIGHT => 1000;
use constant DEFAULT_QSIZE => 4;
use constant DEFAULT_CHILD_TIMEOUT => 15;

use constant DEFAULT_FORWARDING_METHOD => 'masquerading';
use constant DEFAULT_SERVICETYPE => 'tcp';
use constant DEFAULT_SCHEDULER => 'wrr';
use constant DEFAULT_INTERVAL  => 20;
use constant DEFAULT_REBALANCE_INTERVAL => 1;

use constant SLEEP_GRANULARITY => 0.5;
 
use constant FORK_DELAY        => 0.01;

use constant ALMOST_ZERO       => 0.0000000001;

use constant SCALE_UP_ABOVE => 20000;       # kiss scales up the weights
                                            # above this. it should not
                                            # be above 32K. better not
                                            # to touch this one.

# Data

my $VERSION            = "1.32";
my $keep_running       =       0;
my $reload_config      =       0;
my $dump_status        =       0;
my $syslog_ready       =       0;

my $socket;
my $socket_selector;

my $child_pipe;


my %MEXEC_RESULTS;              # results of various tests
                                # this makes the threads work.

my %QS;				# queues.
my %INVALID_QS;                 # if the queue for a VIP is invalid. mark it here
my %STATUS;			# who is alive
my %WEIGHTS;			# current weights
my %ITERATIONS;

my %FORWARDING_METHODS = (
			  gatewaying   => '-g',
			  ipip         => '-i',
			  masquerading => '-m',
			 );

my %SERVICES = ( 
		tcp    => '-t',
		udp    => '-u',
		fwmark => '-f',
		);

my %STATES = (
              0 => "OK",
              1 => "RECOVERED",
              2 => "DEAD",
              3 => "INACTIVE",
             );


# handles for the socket commucations:
my %CLIENT_HANDLING = (
                       status    => \&handle_client_status,
                       qs        => \&handle_client_qs,
                       weights   => \&handle_client_weights,
                       reload    => \&handle_client_reload,
                       shutdown  => \&handle_client_shutdown,
                       disable   => \&handle_client_disable,
                       enable    => \&handle_client_enable,
                       ping      => \&handle_client_ping,
                       states    => \&handle_client_states,
                       debug     => \&handle_client_debug,
                       interval  => \&handle_client_interval,
                       rinterval => \&handle_client_rinterval,
                       config    => \&handle_client_config,
                      );


# Thing you should know.
#
# * A RIP is a RealServer. 
# * A VIP is a VirtualServer. 
#
#
#
#
#
      

        
# Signal handlers:
# genral subroutines at the bottom.

sub catch_zap {
  my ($sig) = @_;
  my_syslog('info', "SIG$sig caught - shutting down");
  $keep_running = 0;
}

sub catch_pipe {
  my ($sig, @err) = @_;
  backtrace_to_syslog("SIGPIPE caught - abuse or bug?");
  # backtrace?
}


sub catch_status {
  my ($sig) = @_;
  $dump_status = 1;
}

sub catch_reload {
  my ($sig) = @_;
  my_syslog('info', "SIG$sig caught - reloading configuration");
  $reload_config = 1;
}

sub catch_die {
  my ($sig, @err) = @_;
  my_syslog('error', "FATAL: $SIG$sig caugth: " . join(" ", chomp @err));
  exit(254);
}

sub catch_warn {
  my ($sig, @err) = @_;

  my_syslog('error', "$SIG$sig caugth: " . join(" ", @err));

}

sub END {
  if ($keep_running) {
    my_syslog('error', "Unclean exit in progress");
    my_syslog('error', "Backtrace follows");
    for ( @{ DB::backtrace() } ) {
      my_syslog('error', $_ );
    }
    my_syslog('error', "[End of backtrace]");
  }
}


# Handle options

our ($DEBUG, $FOREGROUND, $DRY_RUN, $INTERVAL, $RINTERVAL, $USE_SYSLOG,
     $FACILITY, $SOCKET, $NOSOCKET, $HELP, $CONFIG, $GET_VERSION,
     $PIDFILE, $USE_THREADS, $INTERPOLATE );

$USE_SYSLOG  =  1;
$USE_THREADS =  1;
$INTERVAL   = DEFAULT_INTERVAL;
$RINTERVAL  = DEFAULT_REBALANCE_INTERVAL;

$FACILITY   ="local1";
$SOCKET     = "/var/run/lvs-kiss.sock"; 


GetOptions( "help"         =>  \$HELP,
            "config=s"     =>  \$CONFIG,
            "debug"        =>  \$DEBUG,
            "foreground"   =>  \$FOREGROUND,
            "dry-run"      =>  \$DRY_RUN,
            "version"      =>  \$GET_VERSION,
            "interval=i"   =>  \$INTERVAL,
            "rinterval=i"  =>  \$RINTERVAL,
            "syslog!"      =>  \$USE_SYSLOG,
            "threads!"     =>  \$USE_THREADS,
            "facility=s"   =>  \$FACILITY,
            "pidfile=s"    =>  \$PIDFILE,
            "nosocket"     =>  \$NOSOCKET,
            "socket=s"     =>  \$SOCKET,
            "interpolate"  =>  \$INTERPOLATE,
            )
  or   pod2usage(   -msg     => "$!    Type $0 --help for help",
                    -exitval => 1, 
                    -verbose => 0,
                    -output  => \*STDERR );

if ($HELP)  {
  
  pod2usage(   -msg     => '',
               -exitval => 0, 
               -verbose => 1,
               -output  => \*STDOUT );

} elsif ( $GET_VERSION )  {

  print("lvs-kiss version $VERSION (CVS ID: ", CVS_ID, ") \n\n",
        "Copyright (c) 2002, 2003 Linpro AS / Per Andreas Buer \n",
        "This is free software; see the source for copying conditions.  There is NO\n",
        "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n");

  exit(0);
}

STDOUT->autoflush() if ($DEBUG);

my $config = read_config($CONFIG || "/etc/lvs-kiss.conf" );

#print(Dumper($config));
if ( check_config($config) != 0 ) {

  my_syslog('err', 'Errors found in configuration - I REFUSE to start up');
  STDERR->print("Errors found in configuration - I REFUSE to start up\n");

  exit(1);
}


$SIG{TERM} = $SIG{INT} = $SIG{QUIT} = \&catch_zap;
$SIG{PIPE} = \&catch_pipe;
$SIG{USR1} = \&catch_status;
$SIG{HUP}  = \&catch_reload;
$SIG{__WARN__} = \&catch_warn;
$SIG{__DIE__} = \&catch_die;


unless ($NOSOCKET) {

  my_syslog('debug',"Setting up socket")
    if ($DEBUG);
  
  # clear old socket.

  # if (-S $SOCKET) {
  #  unlink( $SOCKET ) 
  #    or my_die("An old socket exists but I could not remove it.");
  # }

  $socket = new IO::Socket::UNIX(
                                 Type => SOCK_STREAM,
                                 Local => $SOCKET,
                                 Peer  => $SOCKET,
                                 Listen => 1,
                                ) 
    or my_die("Could not create socket ($SOCKET)");

  # The socket is up - now set up the select()-stuff.

  $socket_selector = IO::Select->new();
  
  $socket_selector->add( $socket );
}


if ( $USE_SYSLOG ) {
  unless ($DRY_RUN) {
#    setlogsock('unix');
    openlog('lvs-kiss', 'pid', $FACILITY, );

    $syslog_ready ++;
  }
  my_syslog('info', "Hi! lvs-kiss daemon starting up. ");
}


if ($FOREGROUND or $DEBUG ) {
  print("NOTE: I won't background\n");
} else  {
  $0 = "lvs-kiss";
  POSIX::setsid();
  chdir('/');
  close(STDIN);
  close(STDOUT);
  close(STDERR);

  my $pid = fork();
  die("Can't fork: $!") unless defined($pid);
  $pid && exit();
  open(STDIN, '</dev/null');
  open(STDOUT, '>/dev/null');
  open(STDERR, '>/dev/null');
}




# write pid-file


if ( $PIDFILE ) {
  if (open(PID,  ">$PIDFILE")) {
    PID->print($$);
    close(PID);
  } else {
    my_die("Could not write PID-file ($PIDFILE): $!");
  }  
}

# phase 1: set up LVS with equal weighting
ipvs_clear();
# print(Dumper($config->{config}));
# my_exit();

ipvs_setup();

$keep_running = 1;

MAINLOOP: while ($keep_running) {
  
  my $start_of_run = [gettimeofday];

  if ($USE_THREADS) {
    %MEXEC_RESULTS = ();
    %MEXEC_RESULTS = %{ multiexec( build_mexec() ) } 
  }
  my $VIPS = $config->{config}->{VirtualServer};
  
 CHECK_VIP: for my $vip (keys %{ $VIPS }) {
    
    my_syslog('debug',"> Doing VIP: $vip <") if $DEBUG;
    delete( $INVALID_QS{ $vip });   # Note: the queue if always valid here.
    
    my $RIPS= $VIPS->{$vip}->{RealServer};
      
  CHECK_RIP: for my $rip (keys %{ $RIPS }) {

      my_syslog('debug', "Doing RIP:  $vip / $rip <<")
        if $DEBUG;
      
      
      if ($STATUS{$vip}{$rip} == INACTIVE ) {
        
        my_syslog('debug',"Skipping RIP (inactive)") 
          if $DEBUG;
        next;
        
       }

      
      if ( ! defined($STATUS{$vip}{$rip} ) and $RIPS->{$rip}->{Inactive} ) {
        my_syslog('debug',"    Skipping RIP (inactive)") if $DEBUG;
        $STATUS{$vip}{$rip} = INACTIVE;
        next;
      }
    
      
      my ($ret_val, $output, $t ) = 
        safe_system( $RIPS->{$rip}->{Test},
                     $RIPS->{$rip}->{TestTimeout} || DEFAULT_CHILD_TIMEOUT);
      
      my %VALUES = (
                    vip => $vip,
                    rip => $rip,
                    output => $output,
                    date => scalar(localtime(time)),
                    time => $t,
                    retval => $ret_val 
                    );

      if ($ret_val != 0) {
        
        # send syslog warning - but only the first time.
	my_syslog("debug", "    $rip is DEAD - $output") if $DEBUG;
        
        unless ( $STATUS{$vip}{$rip} ) {
          my_syslog('warning', "$vip / $rip is DEAD - taken out -  $output");
          
          if (defined($RIPS->{$rip}->{RunOnFailure} ) ) {
            my $cmd = $RIPS->{$rip}->{RunOnFailure};
            $cmd = process_macros($cmd, \%VALUES);
            safe_system( $cmd, DEFAULT_CHILD_TIMEOUT , 1);
          }
	}
        
        # The RIP is dead. Kill it now. Don't wait.
        ipvs_kill_rip_now($vip,$rip);
	
        $WEIGHTS{$vip}{$rip}   = 0;
        $STATUS{$vip}{$rip}    = DEAD; 
        
        next CHECK_RIP;
        
      } else {
        
	my_syslog("debug", "    $rip is ALIVE") if $DEBUG;
        
        # set the status to RECOVERED if it _was_ DEAD
        if  ($STATUS{$vip}{$rip} == DEAD) {
          $STATUS{$vip}{$rip}    = RECOVERED;
          my_syslog('warning', "$vip / $rip has RECOVERED");

          if (defined($RIPS->{$rip}->{RunOnRecovery} ) ) {

            my $cmd = $RIPS->{$rip}->{RunOnRecovery};
            
            $cmd = process_macros($cmd, \%VALUES);
            safe_system( $cmd,
                         DEFAULT_CHILD_TIMEOUT, 1 );
          }
          # 
          # delete the Q.
          delete($QS{$vip});
          ipvs_set_vip_default_weights( $vip );

        }  else {
          $STATUS{$vip}{$rip}    = OK;
        }
      }


      # This is the section for LOAD-BALANCING. FAIL-OVER is the part
      # above this.
      #
      # (still in for my $rip) 
      #
    

      if (not defined $VIPS->{$vip}->{DynamicScheduler}) {
        my_syslog('debug', 
                  "Skipping loadmeasuring of $vip - we use static load-balancing")
          if $DEBUG;
        next CHECK_RIP;
      }

      if (defined $INVALID_QS{$vip} ) {
        my_syslog('info', "Queue '$vip' is invalid - skippping");
        next CHECK_RIP;
      }

      # if $RINTERVAL != 0 we should not rebalance the cluster at every
      # iteration. this might be useful if the gatering of the load puts
      # a strain on the RIPS (eg. counting the number of mails in the
      # mailspool might be heavy on a server with a lot of mail in the
      # queue.

      # if the load-measurement failes where are in trouble. we should
      # reload the default weigths and jump out of this whole loop. I am
      # not 100% sure if the logic is all sane.

	
      if (($ITERATIONS{$vip} % ( $VIPS->{$vip}->{RebalanceInterval} || 
                                 $RINTERVAL) ) != 0) {
        my_syslog('debug', 
                  "Skipping loadmeasuring of $vip / $rip - it's not time")
          if $DEBUG;
        next CHECK_RIP;
      }
    
      # Here we have to choose. Either we time the operation (nice for
      # http-servers) or have some way of getting the workload of the
      # machine.

      # if the loadcommand is defined - we execute it.

      if (defined $RIPS->{$rip}->{LoadCommand} ) {

        my_syslog('debug',"      (", 
                  $VIPS->{$vip}->{LoadMeasure},") <== ", 
                  $RIPS->{$rip}->{LoadCommand}, 
                 ) if $DEBUG;
        

        my ($ret_val, $output ) = 
          safe_system( $RIPS->{$rip}->{LoadCommand},
                       $RIPS->{$rip}->{LoadCommandTimeout} || 
                       DEFAULT_CHILD_TIMEOUT);

        if ($ret_val == 0) {           
          # everything is OK.
          
          my $RX = $RIPS->{$rip}->{RXFilter} || '(\d+(\.\d+)?)';
          
          ($t) = ( $output =~ m/$RX/sm);
          my_syslog('debug',"      output: $t") if $DEBUG;

        } else {

          # one of the test failed. since the service is up and
          # running we just assume the loadcommand is bogus. we flush
          # out the while Queue.

          # Don't like this? Other sane aproches? - tell me!
          my_syslog('warning', 
                    "Loadmeasure for RIP: $rip failed - the queue for VIP: $vip is now invalid") 
            unless ($INVALID_QS{$vip});

          my_syslog('info', "Setting default weights for VIP '$vip' ");
          
          # bug? Will this re-awaken dead RIPs?
          # besides, the "sub" abouve should use this code as well.
          ipvs_set_vip_default_weights($vip);
          $INVALID_QS{$vip} = 1;
          delete $QS{$vip};
          next CHECK_RIP;
        }
        
      } else { 
        # Time is the default measurement.
        # $t is already set and sane.
      } 
        
      # push the fetched value onto the queue - add fuzz..
      
      push( @{ $QS{$vip}{$rip} } , 
            $t *  ( $RIPS->{$rip}->{LoadFactor} || 1 ) +
            $VIPS->{$vip}->{Fuzz} );
      
      my_syslog('debug',"  Q-size: ", scalar @{ $QS{$vip}{$rip} })
        if $DEBUG;

      while ( scalar @{ $QS{$vip}{$rip} } > (($VIPS->{$vip}->{QueueSize} || DEFAULT_QSIZE) ) ) {
	shift( @{ $QS{$vip}{$rip} } );
        my_syslog('debug',"  Shrinking Queue, size is now ",  scalar @{ $QS{$vip}{$rip} } )
          if $DEBUG;
      }
    } # end of CHECK_RIP
    
    if ($INVALID_QS{$vip}) {
      my_syslog('debug',"  The queue for VIP($vip) is invalid - skipping")
        if $DEBUG;
      next;
    }
    


    if ( ( ++ $ITERATIONS{$vip} % 
           ( $VIPS->{$vip}->{RebalanceInterval}
             || $RINTERVAL) ) != 0) {
      
      my_syslog('debug', "  Skipping VIP ($vip) - not time")        if $DEBUG;
      next;
    }
    
    
    my_exit() unless ($keep_running);
   
    my $vip_sum_inv = 0;
    my %RIPSUMS_inv = ();

    my $liveRIPs = 0;

    my $max = 0;
    my $min = undef;

    my $scale = 1;
    
    my_syslog('debug',"  VIP: $vip") if ($DEBUG);
    
    # Calculate sum for each RIP (if it is dynamic)
    
    if ( $VIPS->{$vip}->{DynamicScheduler} ) {

      for my $rip ( keys %{ $QS{$vip} } ) {
        
        if ($STATUS{$vip}{$rip} == INACTIVE ||
            $STATUS{$vip}{$rip} == DEAD ) {
          my_syslog('debug',"    Skipping RIP (inactive|dead)") 
            if $DEBUG;
          next;
        }

        $liveRIPs ++;
      
        $RIPSUMS_inv{$rip} = 0;
        
        for my $t ( @{ $QS{$vip}{$rip} } ) {
          $RIPSUMS_inv{$rip} += $t;
        }
        
        my_syslog('debug',"RIP($rip): $RIPSUMS_inv{$rip} ") if $DEBUG;
        
        $RIPSUMS_inv{$rip} = 1 / ( $RIPSUMS_inv{$rip} || ALMOST_ZERO);
        
        $max = ( $RIPSUMS_inv{$rip} > $max) ? $RIPSUMS_inv{$rip} : $max;
        $min = ( !defined($min) || $RIPSUMS_inv{$rip} < $min ) ? $RIPSUMS_inv{$rip} : $min;

      }

      
      # we scale the figures so that the result ends up in the range 
      # 10 - 64K
      
      my_syslog('debug',"  Max value '$max'  -- Min value '$min'")
        if ($DEBUG);
      
      # ugg.
      $max = ALMOST_ZERO if ($max == 0);
      $min = ALMOST_ZERO if ($min == 0);
      
      my ($lmax, $lmin) = ($max, $min);

      
      $scale ++ while ($max * (2 ** $scale) < SCALE_UP_ABOVE );
      
      my_syslog("debug", sprintf("  Scale: %d (max: %6.2f   min: %6.2f) ",
                                 $scale, $max * 2 ** $scale,
                                 $min * 2 ** $scale ) )
        if ($DEBUG);
                                 
      my_exit() unless ($keep_running);
                                 
    # readability. Config::General
      my $no_of_rips = scalar(keys( %{ $QS{$vip} } ) );

    my $queue_size = $VIPS->{$vip}->{QueueSize} || DEFAULT_QSIZE;

    # Rebalance the VIP
    for my $rip ( keys %{ $QS{$vip} } ) {
      # in order to calculate the weights we need a couple of numbers.

      if ($STATUS{$vip}{$rip} == INACTIVE ) {
        
        my_syslog('debug',"    Skipping RIP (inactive|deaktivated)") 
          if $DEBUG;
        next;
        
      }
      
      next if ($STATUS{$vip}{$rip} == DEAD ); # skip the RIP if it is dead.
      
      # Are we using Dynamic Scheduling?
      if ( $VIPS->{$vip}->{DynamicScheduler} ) {
	
        # dont try to rebalance unless the queue is full.
	next unless ( @{ $QS{$vip}{$rip} } ==
		      $queue_size );

        
        # this is the the one line of code which does the balancing.
        # you might want to change this.

        
        
        my $w = $RIPSUMS_inv{$rip} * 2 ** $scale;
        $WEIGHTS{$vip}{$rip} = $w;

        my_syslog('debug',"    Weight: $RIPSUMS_inv{$rip} * 2 ** $scale = $w ")
          if $DEBUG;
	
	ipvs_set_weight($vip, $rip, int($WEIGHTS{$vip}{$rip}));
      
      } else {

	ipvs_set_weight($vip, $rip, $RIPS->{$rip}->{DefaultWeight} ||
                        DEFAULTWEIGHT);
      }
      my_syslog('debug',"    End of run for RIP($vip/$rip)")
        if $DEBUG;    
    } # for my $rip
    

  }
    # sleep a bit in order to check for commucations on the socket.
    my_sleep( SLEEP_GRANULARITY )
      unless $USE_THREADS;
    
    my_syslog('debug',"  End of run for VIP($vip)")
      if $DEBUG;
  }

  my_syslog('debug',"End of run - global")
    if $DEBUG;
  my $sleeptime = ( $INTERVAL - tv_interval($start_of_run) );
  $sleeptime = ($sleeptime < 1) ? 1 : $sleeptime;

  my_exit() unless ($keep_running);
  my_syslog('debug', 
            "Will sleep for $sleeptime seconds (interval: $INTERVAL) ")
    if ($DEBUG);

  if ($dump_status) {
    dump_status();
    $dump_status = 0;
  }

  my_sleep( $sleeptime );

  if ($dump_status) {
    dump_status();
    $dump_status = 0;
  }
  my_exit() unless ($keep_running);


  # end of run. check if we are to reload the configuration.

  if ($reload_config) {
    reload_config();
    next MAINLOOP;
  }

}


# build a mexec hash which can be fed into multiexec
sub build_mexec {

  my (%H);
  my $maxtimeout = 0 || DEFAULT_CHILD_TIMEOUT;

  my $VIPS = $config->{config}->{VirtualServer};

 VIPLOOP: for my $vip (keys %{ $VIPS } ) {
    my $RIPS = $VIPS->{$vip}->{RealServer};
  RIPLOOP1: for my $rip (keys %{ $RIPS } ) {
      
      $maxtimeout = ( $RIPS->{$rip}->{TestTimeout} > $maxtimeout ) ? 
        $RIPS->{$rip}->{TestTimeout} : $maxtimeout;

      if ($STATUS{$vip}{$rip} == INACTIVE) {
        my_syslog('debug', "  test: Skipping $vip / $rip - inactive") 
		if $DEBUG;
	next;
      }
      
      $H{ $RIPS->{$rip}->{Test} } = 
        [
         $RIPS->{$rip}->{Test}, 
         $RIPS->{$rip}->{TestTimeout} || DEFAULT_CHILD_TIMEOUT
        ];
      

    }
    
    if ( ( $ITERATIONS{$vip} % 
           ( $VIPS->{$vip}->{RebalanceInterval}
             || $RINTERVAL) ) != 0 ) {
      next VIPLOOP;

    }
    

  RIPLOOP2: for my $rip (keys %{ $RIPS } ) {

      if ( ( ! $RIPS->{$rip}->{LoadCommand} ) ||
           ($STATUS{$vip}{$rip} == INACTIVE) ||
           ($STATUS{$vip}{$rip} == DEAD) ) {

        my_syslog('debug', "  loadcommand: Skipping $vip / $rip - inactive/dead/not defined")
		if $DEBUG;

        next RIPLOOP2;

      }
      
      $maxtimeout = ( $RIPS->{$rip}->{LoadCommandTimeout} > $maxtimeout) ? 
        $RIPS->{$rip}->{LoadCommandTimeout} : $maxtimeout;

      $H{ $RIPS->{$rip}->{LoadCommand} } = 
        [
         $RIPS->{$rip}->{LoadCommand}, 
         $RIPS->{$rip}->{LoadCommandTimeout} || DEFAULT_CHILD_TIMEOUT
        ];
    }
    return($maxtimeout, \%H, );
  }
}


sub reload_config {
  my $ret = 0;

  $reload_config = 0;
  
  my_syslog('info', 'Reloading configuration ('. 
            ( $CONFIG || "/etc/lvs-kiss.conf" )
            . ')', );

  %QS         = ();
  %INVALID_QS = ();
  %STATUS     = ();
  %WEIGHTS    = ();
  %ITERATIONS = ();

  my $new_config = 
    read_config( $CONFIG || "/etc/lvs-kiss.conf");
  
  if ( check_config( $new_config ) == 0 ) {
    $config = $new_config;
    my_syslog('info', 'Configuration reloaded - OK');

    ipvs_clear();
    ipvs_setup();

    return 0;
  } else {
    my_syslog('err', 'Errors found in configuration - NOT reloading');
    return 1;
  }
}



sub read_config {
  my ($file) = @_;

  my $new_config =  new 
    Config::General( -ConfigFile => $file,
                     -AutoTrue => 0,
                   );
  
  return $new_config;
}



sub ipvs_delete_vip {
  my ($vip) = @_;
  my $service = get_service_type($vip);
  my $sch = get_sch_type($vip);

  run_ipvsadm(" -A $service $vip -s $sch");  

}

sub ipvs_setup_vip {
  my ($vip) = @_;

  ipvs_new_vip($vip);

  my $RIPS = $config->{config}->{VirtualServer}->{$vip}->{RealServer};

RIPLOOP:  for my $rip (keys %{ $RIPS } ) {
  
    my $weight =  ( $RIPS->{$rip}->{Inactive} ) ?
               0 : ( $RIPS->{$rip}->{DefaultWeight} || DEFAULTWEIGHT );
    ipvs_new_rip($vip, $rip, $weight );
    $WEIGHTS{$vip}{$rip} = $weight;
  }
}
  
sub ipvs_setup {
  for my $vip (keys %{ $config->{config}->{VirtualServer} } ) {
    ipvs_setup_vip($vip);
  }
}

sub get_forw_type {
  my ($vip, $rip) = @_;
  return $FORWARDING_METHODS{
			     ( $config->{config}->{VirtualServer}->
                               {$vip}->{RealServer}->{$rip}->
                               {PacketForwardingMethod} || DEFAULT_FORWARDING_METHOD )
                            };
}


sub get_sch_type {
  my ($vip) = @_;
  return ( $config->{config}->{VirtualServer}->
           {$vip}->{Scheduler} || DEFAULT_SCHEDULER );
      
}


sub get_persistance {
  my ($vip) = @_;
  return ( $config->{config}->{VirtualServer}->
           {$vip}->{Persistance} || undef );
      
}


sub get_service_type {
  my ($vip, $rip) = @_;

  if ($rip) {
    return ( $SERVICES{
                       ($config->{config}->{VirtualServer}->
                        {$vip}->{RealServer}->{$rip}->
                        {ServiceType} 
                        ||
                        $config->{config}->{VirtualServer}->
                        {$vip}->{ServiceType}
                        ||
                        DEFAULT_SERVICETYPE )
                      } );
  } else {
    return $SERVICES{
                     $config->{config}->{VirtualServer}->
                     {$vip}->{ServiceType}
                    };
  }
}
  
sub ipvs_kill_rip_now {
    my ($vip, $rip) = @_;
    ipvs_set_weight($vip, $rip, 0);
  }


sub ipvs_new_vip {
  my ($vip) = @_;
  my $service = get_service_type($vip);
  my $sch = get_sch_type($vip);
  my $persistance = get_persistance ($vip);

  my $p_string = ($persistance) ? "-p $persistance" : '';
  run_ipvsadm(" -A $service $vip -s $sch $p_string");

}

sub ipvs_set_vip_default_weights {
  my ($vip) = @_;

  for my $rip (keys %{ $config->{config}->{VirtualServer}->
			   {$vip}->{RealServer} }) {
    next if ($STATUS{$vip}{$rip} != OK and
             $STATUS{$vip}{$rip} != RECOVERED);

    ipvs_set_weight(
                    $vip, $rip, 
                    $config->{config}->{VirtualServer}->
                    {$vip}->{RealServer}->{$rip}->{Defaultweight} || DEFAULTWEIGHT
                   );
  }

}




sub ipvs_new_rip {
  my ($vip, $rip, $weigh) = @_;
  my $forw = get_forw_type($vip, $rip);
  my $service = get_service_type($vip, $rip);
  run_ipvsadm(" -a $service $vip -r $rip $forw -w $weigh");
}

sub ipvs_set_weight {
  my ($vip, $rip, $weigh) = @_;
  my $forw = get_forw_type($vip, $rip);
  my $service = get_service_type($vip, $rip);
  run_ipvsadm(" --edit-server $service $vip --real-server $rip $forw --weight $weigh");
}

sub ipvs_clear {
  my_syslog('debug', "Clearing IPVS tables")
    if $DEBUG;
  run_ipvsadm(" -C");

}

sub dump_status {
  my $tmpfile = "/tmp/lvs-kiss.status"; # should we make a uniqe name?
  return(1) if (-l $tmpfile);

  my_syslog('info', "Dumping status in $tmpfile");
  open(STATUS, ">$tmpfile");

  STATUS->print("\n------------- lvs-kiss status ------------\n");
  STATUS->print("------------- ",scalar localtime(time)," ------------\n");
  
  for my $vip (keys %{ $config->{config}->{VirtualServer} }) {
    STATUS->print("$vip\n");
    for my $rip (keys %{ $config->{config}->{VirtualServer}->
			   {$vip}->{RealServer} }) {
      STATUS->print("    $rip\n",
            "        Status: ", $STATES{$STATUS{$vip}{$rip}}, "\n",
            "        Current weight: ",int($WEIGHTS{$vip}{$rip}), "\n",
           );
      
      my $sum = 0;
      STATUS->print("        Queue: ");
      for my $t ( @{ $QS{$vip}{$rip} } ) {
        $sum += $t;
        STATUS->printf("[%4.2f] ",$t);
      }
      STATUS->printf("  => [%4.2f] \n",$sum);
    }
    STATUS->print("\n\n");
  }
  close(STATUS);
}




sub run_ipvsadm {

  my $cmd = 'ipvsadm ' . join(' ',@_);
  
  my_syslog('debug',"          system($cmd)\n")
    if $DEBUG;
  
  if ($DRY_RUN) {
    #uhh
  } else {
    system("$cmd");
  }
  return($CHILD_ERROR);
}

sub my_sleep {
  my ($sleeptime) = @_;
  my $sleep_until = time() + $sleeptime;
  
  while ( (time() < $sleep_until) and $keep_running ) {
    #    print(".") if $DEBUG;
    unless ($NOSOCKET) {

      if ( $socket_selector->can_read( SLEEP_GRANULARITY ) ) {
        handle_socket();
      }

    } else {
      sleep( SLEEP_GRANULARITY );
    }
  }
  # print("\n") if $DEBUG;

}


sub my_system {
  # my_system is a wrapper around system() with a 
  # bit of logging.
  
  my $cmd = join(' ',@_);

  my_syslog('debug',"    my_system($cmd)\n") 
    if $DEBUG;

  system("$cmd > /dev/null 2>&1");
  return($CHILD_ERROR);
}



sub multiexec {
  # a subroutine for executing many programs at once.
  # we use select() to collect output.

  my ($timeout, $cmd_ref) = @_;
  my $now = time();
  my %PIPES;
  my $s = new IO::Select();
  my (%RET, %OUTPUT, %RV, %TIMES, %START);
  my $t0 = [ gettimeofday ];
  
  my @pids;

  my_syslog('debug',"    multiexec called with a timeout of $timeout\n") 
    if $DEBUG;


  for my $cmd (keys %{ $cmd_ref } ) {
    my $pipe = new IO::Pipe;

    my $pid;
    
    # sleep a tiny bit in order to reduce the strain on the host.

    sleep(FORK_DELAY);

    if( $pid = fork() ) { 
      
      $pipe->reader();

      $s->add($pipe);

      $PIPES{ $pipe } = 
        [ $cmd, 
          $pid,
          $now +  $cmd_ref->{$cmd},
          [gettimeofday],
        ];

    } elsif(defined $pid) { 
      # Child
      $pipe->writer();

      $SIG{ALRM} = sub { 
        die("Aieee! Slave master thread timed out! This should not happend!\n") 
      };
      alarm( $cmd_ref->{$cmd}[1] + 5);

      my ($rv, $output) = safe_system(@{ $cmd_ref->{$cmd} });
      $pipe->print($output);
      $keep_running = 0;
      exit( $rv );
    }
  }
  my $done = 0;

  until( $s->count == 0 ) {
    
    for my $fh ($s->can_read(1)) {

      if ($fh->eof) {

        $s->remove($fh);
        waitpid( $PIPES{$fh}->[1],0 );

        $RV{$PIPES{$fh}->[0] } = $CHILD_ERROR >> 8;

        $TIMES{$PIPES{$fh}->[0]} = tv_interval($PIPES{$fh}->[3]);

      } else {
        
        # do something with the data.
        push( @{ $OUTPUT{ $PIPES{$fh}->[0] } }, <$fh> );
      }
      
    }
  }
  for my $name (keys %RV) {
    $RET{ $name } = [ $RV{$name}, ($OUTPUT{$name}) 
                      ? join('', @{ $OUTPUT{$name} } ) : undef,
                      $TIMES{$name},
                    ];
  }
  
  # a hash with lists. each list is 
  # [ retval, output, execution time ]
  return(\%RET);
}

sub safe_system {
  my ($command, $timeout, $direct) = @_;

  
  if (!$direct && defined( $MEXEC_RESULTS{ $command } )) {
    
   my_syslog('debug', "safe_system found pre-executed result in cache")
     if $DEBUG;
   return @{ $MEXEC_RESULTS{ $command } };
 }


  my ($pid,$output,$return_val);
  
  my $t0 = [gettimeofday];

  my_syslog('debug',"      safe_system($command, $timeout)\n")
    if $DEBUG;


  my ($method, $commandline) = 
    ($command =~ m/^(perl:|sh:)?(.*)/i);
  $method ||= 'sh:';

  my_syslog('debug',"      method is $method   - commandline is $commandline\n")
    if $DEBUG;


  my_die("Can't fork: $!")
    unless defined($pid = open(KID, "-|"));
  $child_pipe = \*KID;

  if ($pid) {
    # parent
    my $trapped = 0;

    my_syslog('debug',"        Parent arming trap\n") if $DEBUG;
    arm_trap($pid,$timeout,
             $command,
             \$trapped ); # Arm trap. CHILD_TIMEOUTs timeout.
    
    #eval this
    eval{ $output = join('', <KID> ); };
    KID->close();
    my_syslog('debug',"        Output returned: $output\n") if $DEBUG;

    disarm_trap($pid);
    
    if ($trapped) {
      # one of the test failed. since the service is up and
      # running we just assume the loadcommand is bogus. we flush
      # the queue.

      $return_val = 256;
      $output = "'$command' timed out!";

    } else {
      $return_val = $CHILD_ERROR >> 8;
      my_syslog('debug',        "Return value is $return_val") if $DEBUG;
    }

    # reap dead children
    while (waitpid(-1, WNOHANG) > 0 ) { sleep(0.1) };

  } else {
      
    # child

    # redirect STDERR til STDOUT
    POSIX::dup2( \*STDERR, \*STDOUT);

    if (lc($method) eq 'perl:') {
      $keep_running = 0;

      STDOUT->print(eval($command));
      STDOUT->close();

      my_syslog("error", "Syntax error in '$command' - $@")
        if ($@);
      exit( ($@) ? 1 : 0 );
      
    } else {
      $keep_running = 0;
      $ENV{PATH} = "/bin:/usr/bin"; # Minimal PATH.
      exec( $commandline );

    }
    
    # this should never be reached.
    my_syslog("debug", "exec of $commandline failed");
    exit(1);

  }
  
  return ($return_val, $output, tv_interval($t0));
}



sub my_syslog {
  my ($level, @msg) = @_;

  if (!$DRY_RUN and $syslog_ready) {
    syslog($level, join(' ', @msg) )
           if  ($USE_SYSLOG);

  } else {
    chomp(@msg);
    print("SYSLOG($level): ",join(" ", @msg), "\n");
  }
}

sub my_die {
  my ($msg) = @_;
  my_syslog('error', $msg);

  closelog() if ($USE_SYSLOG);
  exit(1);
}

sub my_exit {
  # exit in a nice way.
  my_syslog('info', 'lvs-kiss is exiting');
    
  unless ($NOSOCKET) {
    $socket->shutdown(2);

    unlink($SOCKET) or 
      my_syslog('warning', "Could not unlink socket");
  }
  closelog() if ($USE_SYSLOG);
  
  exit();
}

sub my_kill($$) {
  my ($pid, $progname) = @_;
  my_syslog('warning', "Timeout! I am killing $progname ($pid) with SIGKILL");
  kill(9, $pid);
  
}

sub arm_trap {
  my ($pid,$tmout, $msg, $kill_ref) = @_;
  
  $SIG{ALRM} = sub { my_kill($pid, $msg); $$kill_ref = 1 };
  alarm($tmout);

}


sub disarm_trap {
  alarm(0);
  $SIG{ALRM} = 'IGNORE';
}



sub check_config {
  my ($cfg) = @_;

  my $errors = 0;
  my ( %TOPLEVEL, %VIPLEVEL, %RIPLEVEL );

  my_syslog('debug',"Checking config . . . \n")
    if $DEBUG;

  for (qw(VirtualServer)) {
    $TOPLEVEL{$_} = 1;
  }

  for (qw(ServiceType Scheduler DynamicScheduler QueueSize
          Fuzz RealServer RebalanceInterval Persistance) ) {
    $VIPLEVEL{$_} = 1;
  }

  
  for ( qw(PacketForwardingMethod Test LoadCommand RXFilter LoadFactor
           RunOnFailure RunOnRecovery Inactive DefaultWeight TestTimeout
           LoadCommandTimeout
           ) ) {

    $RIPLEVEL{$_} = 1;
  }

  for my $key (keys %{ $cfg->{config} } ) {
    
    if (! defined $TOPLEVEL{$key}) {
      config_error('top level', $key);
      $errors++;
    }

    if ($key eq 'VirtualServer') {
      
      for my $vip ( keys %{ $cfg->{config}->{VirtualServer} } ) {
      
        for my $vip_key ( keys %{ $cfg->{config}->{VirtualServer}->{$vip} } ) {
          
          if (! defined $VIPLEVEL{$vip_key}) {
            config_error("VIP: $vip", $vip_key);
            $errors++;
          }
          
          if ($vip_key eq 'RealServer') {
            for my $rip ( keys %{ $cfg->{config}->{VirtualServer}
                                    ->{$vip}->{RealServer} } ) {

              for my $rip_key ( keys %{ $cfg->{config}->{VirtualServer}
                                          ->{$vip}->{RealServer}->{$rip} } ) {
                
                if (! defined $RIPLEVEL{$rip_key}) {
                  config_error("VIP/RIP: $vip/$rip", $rip_key);
                  $errors++;
                }
              }
            }
          }
        }
      } 
    }
  }

  my_syslog('debug',". . . done\n")
    if $DEBUG;
  
  return $errors;
}


sub process_macros {
  my ($string, $val_ref) = @_;
    
  $string =~ s/%{(\w+)}/$val_ref->{$1}/megs;
  $string =~ s/\n/ /megs;

  return $string;
}


sub config_error {
  my ($context, $keyword) = @_;
  
  my_syslog('err', "error found in config file. Unknown keyword '$keyword' in context '$context'");

}


sub client_error {
  my ($status) = @_;
  return(freeze(\$status));
}


sub client_ok {
  my $status = 0;
  return(freeze(\$status));
}

sub handle_socket {
  # there is an event on the socket - pick it up.
  my $line;

  my $client = $socket->accept();
  $line = <$client>;
  chomp( $line );


  my_syslog('debug',"Got input from socket: $line")
    if $DEBUG;

  my ($cmd, @opt) = split(/\s+/, $line);
  
  if (defined($CLIENT_HANDLING{ $cmd })) {

    eval {
      $client->print(
                     &{ $CLIENT_HANDLING{ $cmd } }(@opt) 
                    )
        };

    my_syslog('error',  
              "Oops. Evil client sent me bogus data and tried to kill me: $@")
      if ($@);
    
  } else {
    my_syslog('error', "Got unknown command from socket: $cmd ( " . 
              join(", ", @opt) . " )");

    $client->print( client_error(1) );

  }
  
  $client->close();

}

sub handle_client_status {
  my ($vip, $rip) = @_;

  if (defined($rip) ) {
    
    if (defined $STATUS{$vip}{$rip}) {
      return( freeze( \$STATUS{$vip}{$rip} ) );
    } else {
      my_syslog('warn', 
                "Client asked for the status of $vip / $rip which is undefined");
      return( client_error( 17 ));
    }

  } elsif (defined($vip)) {

    if (defined $STATUS{$vip} ) {
      return( freeze( $STATUS{$vip} ) );
    } else {
      my_syslog('warn', 
                "Client asked for the status of $vip - which is undefined");
      return( client_error( 17 ));
    }

  } else {

    return( freeze( \%STATUS ) );

  }
}


sub handle_client_qs {
  my ($vip, $rip) = @_;

    if (defined($rip) ) {
    
    if (defined $QS{$vip}{$rip}) {

      return( freeze( \$QS{$vip}{$rip} ) );

    } else {

      my_syslog('warn', 
                "Client asked for the status of $vip / $rip which is undefined");
      return( client_error( 17 ));
    }

  } elsif ( defined($vip) ) {

    if (defined $QS{$vip} ) {
      return( freeze( $QS{$vip} ) );

    } else {
      my_syslog('warn', 
                "Client asked for the status of $vip - which is undefined");
      return( client_error( 17 ));
    }

  } else {

    return( freeze( \%QS ) );

  }

}

sub handle_client_weights {
  my ($vip, $rip) = @_;

  if (defined($rip) ) {
    
    if (defined $WEIGHTS{$vip}{$rip}) {

      return( freeze( \$WEIGHTS{$vip}{$rip} ) );

    } else {

      my_syslog('warn', 
                "Client asked for the status of $vip / $rip which is undefined");

      return( client_error( 17 ));
    }

  } elsif ( defined($vip) ) {

    if (defined $WEIGHTS{$vip} ) {
      return( freeze( $WEIGHTS{$vip} ) );

    } else {
      my_syslog('warn', 
                "Client asked for the status of $vip - which is undefined");
      return( client_error( 17 ));
    }

  } else {
    return( freeze( \%WEIGHTS ) );

  }

}


sub handle_client_shutdown {
  my_syslog('info', "client ordered a shutdown - shutting down");
  $keep_running = 0;
  return( client_ok() );

}

sub handle_client_reload {
  my_syslog('info', "client ordered configuration reload");
  return( client_error( reload_config() ) );
}

# disable a RIP (temporary)
sub handle_client_disable {
  my ($vip, $rip) = @_;
  my $status = 0;

  if (defined( $STATUS{$vip}{$rip} )) {

    ipvs_kill_rip_now($vip,$rip);
	
    $WEIGHTS{$vip}{$rip}   = 0;
    
    $STATUS{$vip}{$rip} = INACTIVE;

    my_syslog('info', 
              "Disabled $vip / $rip");
    
  } else {
    my_syslog('error', 
              "Client tried to disable $vip / $rip -  which is undefined");

  }
  return(freeze(\$status));
}

# enable a RIP
sub handle_client_enable {
  my ($vip, $rip) = @_;
  my $status = 0;

  if (defined( $STATUS{$vip}{$rip} )) {
    
    $STATUS{$vip}{$rip} = DEAD;

    my_syslog('info', 
              "Enabled $vip / $rip - it will enter normal duty soon");
  } else {

    my_syslog('error', 
              "Client tried to enable $vip / $rip -  which is undefined");

  }
  return(freeze(\$status));
}

sub handle_client_ping {
  my_syslog('info', 
            "got PING - sending PONG");

  return( client_ok() );
}

sub handle_client_states {

  return(freeze(\%STATES));

}

sub handle_client_debug {
  ($DEBUG) = @_;
  $DEBUG = 1 
    unless defined ($DEBUG);
  return( client_ok() );
}


sub handle_client_interval {
  $INTERVAL = shift || DEFAULT_INTERVAL;
  my_syslog("info", "check interval set to $INTERVAL");
  return( client_ok() );
}

sub handle_client_rinterval {
  $RINTERVAL = shift || DEFAULT_REBALANCE_INTERVAL;
  my_syslog("info", "rebalancing interval set to $RINTERVAL");
  return( client_ok() );
}

sub handle_client_config {
  my $string = $config->save_string();
  return( freeze( \$string ) );
}

sub backtrace_to_syslog {
  my ($why) = @_;
  my_syslog('error', $why);
  my_syslog('error', "Backtrace follows");
  for ( @{ DB::backtrace() } ) {
    my_syslog('error', $_ );
  }
  my_syslog('error', "[End of backtrace]");
}


# debug magic.


1;

package DB;

sub backtrace { 
  my ($file, $line) = (__FILE__, __LINE__);
  my @trace;
  
  my $pack = '';
  my $i = 0;
  while (1) {
    @DB::args = ();
    
    my @caller = caller($i);
    
    last unless @caller;
    
    my ($npack, $nfile, $nline, $subroutine, $hasargs,
        $wantarray, $evaltext, $is_require) = @caller;
    
    my $trace = '';
    $trace .= "  [$file:$line] ";
    $trace .= $wantarray ? "\@ " : "\$ ";
    $trace .= "$subroutine ";
    $trace .= "\"$evaltext\" " if defined($evaltext);
    $trace .= "(".join(", ",
                       map { (my $a = $_ || "") =~ s/\n/\\n/g; "\"$a\"";
                           } @DB::args).")"
                             if $hasargs;
    
    push(@trace, $trace);

    $i++;
    
    $file = $nfile;
    $line = $nline;
    $pack = $npack;
  }
  push(@trace, "  [$file:$line]   ${pack}::");
  return(\@trace);
}








__END__


=head1 NAME

lvs-kiss - Linux Virtual Server made simple 

=head1 SYNOPSIS

lvs-kiss [--options]

=head1 DESCRIPTION

lvs-kiss is designed to make load-balancing with fail-over simpler. The
primary design-goal is for you to get load-balancing with failover up
and running within an hour or so. 

Secondary design goal is to be able to make this software as flexible and
powerful as possible. Embedding of perl in configuration-files and
load-balancing with custom made tests should be possible without to much
hassle. 

lvs-kiss is configured through a simple, apache-style configuration
file. An load-balancer with failover for apache could look like this:

<VirtualServer web.foo.com:80>
  DynamicScheduler    1

 <RealServer web1.foo.com:80>
   Test wget -t10 -q -O /dev/null http://web1.foo.com/ping.pl
 </RealServer>

 <RealServer web2.foo.com:80>
   Test wget -t10 -q -O /dev/null http://web2.foo.com/ping.pl
 </RealServer>

</VirtualServer>

For details about the configuration, please see the lvs-kiss.conf
manual page.

Once it is running, lvs-kiss can be controlled via the program
lvs-kiss-control. lvs-kiss-control communicates with lvs-kiss via a
socket. Operations such as reloading the configuration, shutting down,
disabling and enabling servers can be done. Please see the
lvs-kiss-control manual page for details.

=head1 OPTIONS

=over 5

=item B<--config <configfile>> 

Specify alternate configuration file. Default if /etc/lvs-kiss.conf. 

=item B<--interval>

Time between checks, in seconds. The default is 20 seconds.

=item B<--rinterval>

How often should a check include a complete rebalancing. If the
loadmeasure is heavy on the realservers and you would rather see that is
not performed so often you can set the I<rinterval> to N. lvs-kiss then
only performs rebalancing every N times. So - if you set I<interval> to
10 (seconds) you can set I<rinterval> to 6 and rebalacing will only
occur once every 60 seconds. 

You can also set a spesific rebalacing interval for each virtual server.

=item B<--syslog> or B<--nosyslog>

Enables or disables syslog. Default is to syslog.

=item B<--threads> or B<--nothreads>

Enables or disables the threading engine. Per default the threads are
enabled. lvs-kiss uses traditional unix threads (fork). Unless you are
debugging you probably don't need to disable threads.

=item B<--interpolate>

Turns off interpolating of variables in the configuration. See the
manual entry for Config::General::Interpolate.

=item B<--pidfile> <pidfile>

Placement of pidfile. Default is /var/run/lvs-kiss.pid

=item B<--facility> <facility>

Specifies what syslog facility lvs-kiss will use. Default is "local1".

=item B<--dry-run>

Do not alter the Kernel Virtual Server Setup. All the tests are still
run. You might wa nt to use --debug with this option.

=item B<--version> 

Show version number and exit.

=item B<--foreground> 

Do not daemonize. Stay in the foreground.

=item B<--debug> 

Turns on debugging. Implies --foreground.

=item B<--help>

Gives help.

=back

=head1 ENVIROMENT

lvs-kiss overrides the PATH. /bin:/usr/bin is used as path.

=head1 SIGNALS

lvs-kiss responds to the following signals.

=over 5

=item B<SIGTERM>

Programs exits in a safe manner.

=item B<SIGUSR1>

Drop a nice status-report to /tmp/lvs-kiss.status.$$ Yes, I've been a
good boy and lvs-kiss is checking if it is a symbolic link before
overwriting it.

=item B<SIGHUP>

Programs re-reads its configuration files.

=item B<SIGKILL>

Be carefull sending SIGKILL to lvs-kiss. It might do some damage and
leave your virtual server in a inconsistant state.

=back

=head1 FILES

/etc/lvs-kiss.conf

=head1 VERSION

This is lvs-kiss 1.2

=head1 AUTHOR

Per Andreas Buer <perbu (at) linpro.no>

=head1 BUGS

Please report bugs and functionality requests to the author.

=head1 COPYRIGHT

Copyright  2002 Per Andreas Buer / Linpro AS.

This is free software; see the source for copying conditions. There is
NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE.


=head1 SEE ALSO

lvs-kiss-control (8), lvs-kiss.conf (5), ipsvadm (8), Config::General (3), 
Config::General::Interpolate (3),

=cut

