#!/usr/bin/perl
#
# Module: vyatta-keepalived.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.
#
# This code was originally developed by Vyatta, Inc.
# Portions created by Vyatta are Copyright (C) 2007-2009 Vyatta, Inc.
# All Rights Reserved.
# 
# Author: Stig Thormodsrud
# Date: October 2007
# Description: Script to glue vyatta cli to keepalived daemon
# 
# **** End License ****
#

use lib "/opt/vyatta/share/perl5/";
use Vyatta::Config;
use Vyatta::Keepalived;
use Vyatta::TypeChecker;
use Vyatta::Interface;
use Vyatta::Misc;
use Getopt::Long;

use strict;
use warnings;

my ($action, $vrrp_intf, $vrrp_group, $vrrp_vip);
my ($conf_file, $changes_file);
my %HoA_sync_groups;

sub validate_source_addr {
    my ($ifname, $source_addr) = @_;

    my @ipaddrs;
    if (defined $source_addr) {
	my %config_ipaddrs;
	my @ipaddrs = Vyatta::Misc::getInterfacesIPadresses('all');
	foreach my $ip (@ipaddrs) {
	    if ($ip =~ /^([\d.]+)\/([\d.]+)$/) { # strip /mask
		$config_ipaddrs{$1} = 1;
	    }
	}
	if (!defined $config_ipaddrs{$source_addr}) {
	    vrrp_log("no hello-source");
	    return "hello-source-address [$source_addr] must be " . 
		"configured on the interface\n";
	}
	return;
    }
    # if the hello-source-address wasn't configured, check that the
    # interface has an IPv4 address configured on it.
    my $intf = new Vyatta::Interface($ifname);
    @ipaddrs = $intf->address(4);
    if (scalar(@ipaddrs) < 1) {
	vrrp_log("no primary or hello-source");
	return "must configure either a primary address on [$ifname] or" .
	    " a hello-source-address\n";
    }
    return;
}

sub keepalived_get_values {
    my ($intf, $path) = @_;

    my @errs   = ();
    my $output = '';
    my $config = new Vyatta::Config;

    my $state_transition_script = get_state_script();
    
    vrrp_log("keepalived_get_values [$intf][$path]");
    $config->setLevel("$path vrrp vrrp-group");
    my @groups = $config->listNodes();
    foreach my $group (@groups) {
	my $vrrp_instance = "vyatta-$intf-$group";
	$config->setLevel("$path vrrp vrrp-group $group");
	if ($config->exists("disable")) {
	    vrrp_log("$vrrp_instance disabled - skipping");
	    my $state_file = get_state_file($intf, $group);
	    system("rm -f $state_file");
	    next;
	}
	my @vips = $config->returnValues("virtual-address");
	my $num_vips = scalar(@vips);
	if ($num_vips == 0) {
	    push @errs, "must define a virtual-address for vrrp-group $group\n";
	    next;
	}
	if ($num_vips > 20) {
	    push @errs, "can not set more than 20 VIPs per group\n";
	    next
	}
	my $priority = $config->returnValue("priority");
	if (!defined $priority) {
	    $priority = 1;
	}
	my $preempt = $config->returnValue("preempt");
	if (!defined $preempt) {
	    $preempt = "true";
	}
	my $preempt_delay = $config->returnValue("preempt-delay");
	if (defined $preempt_delay and $preempt eq "false") {
	    print "Warning: preempt delay is ignored when preempt=false\n";
	}
	my $advert_int = $config->returnValue("advertise-interval");
	if (!defined $advert_int) {
	    $advert_int = 1;
	}
	my $sync_group = $config->returnValue("sync-group");
	if (defined $sync_group && $sync_group ne "") {
	    push @{ $HoA_sync_groups{$sync_group} }, $vrrp_instance;
	}
	my $hello_source_addr = $config->returnValue("hello-source-address");
	my $err = validate_source_addr($intf, $hello_source_addr);
	if (defined $err) {
	    push @errs, $err;
	    next;
	}

	$config->setLevel("$path vrrp vrrp-group $group authentication");
	my $auth_type = $config->returnValue("type");
	my $auth_pass;
	if (defined $auth_type) {
	    $auth_type = "PASS" if $auth_type eq "simple";
	    $auth_type = uc($auth_type);
	    $auth_pass = $config->returnValue("password");
	    if (! defined $auth_pass) {
		push @errs, "vrrp authentication password not set\n";
		next;
	    }
	}

	$config->setLevel("$path vrrp vrrp-group $group run-transition-scripts");
        my $run_backup_script = $config->returnValue("backup");
        if(!defined $run_backup_script){
           $run_backup_script = "null";
        }
        my $run_fault_script = $config->returnValue("fault");
        if(!defined $run_fault_script){
           $run_fault_script = "null";
        }
        my $run_master_script = $config->returnValue("master");
        if(!defined $run_master_script){
           $run_master_script = "null";
        }

	# We now have the values and have validated them, so 
	# generate the config.

	$output  .= "vrrp_instance $vrrp_instance \{\n";
	my $init_state;
	$init_state = vrrp_get_init_state($intf, $group, $vips[0], $preempt);
	$output .= "\tstate $init_state\n";
	$output .= "\tinterface $intf\n";
	$output .= "\tvirtual_router_id $group\n";
	$output .= "\tpriority $priority\n";
	if ($preempt eq "false") {
	    $output .= "\tnopreempt\n";
	}
	if (defined $preempt_delay) {
	    $output .= "\tpreempt_delay $preempt_delay\n";
	}
	$output .= "\tadvert_int $advert_int\n";
	if (defined $auth_type) {
	    $output .= "\tauthentication {\n";
	    $output .= "\t\tauth_type $auth_type\n";
	    $output .= "\t\tauth_pass $auth_pass\n\t}\n";
	}
	if (defined $hello_source_addr) {
	    $output .= "\tmcast_src_ip $hello_source_addr\n";
	}
	$output .= "\tvirtual_ipaddress \{\n";
	foreach my $vip (@vips) {
	    $output .= "\t\t$vip\n";
	}
	$output .= "\t\}\n";
	$output .= "\tnotify_master \"$state_transition_script master ";
	$output .=     "$intf $group $run_master_script @vips\" \n";
	$output .= "\tnotify_backup \"$state_transition_script backup ";
        $output .=     "$intf $group $run_backup_script @vips\" \n";
	$output .= "\tnotify_fault \"$state_transition_script fault ";
	$output .=     "$intf $group $run_fault_script @vips\" \n";
	$output .= "\}\n\n";
    }

    return ($output, @errs);
}

sub vrrp_get_sync_groups {
    
    my $output = "";
   
    foreach my $sync_group ( keys %HoA_sync_groups) {
	$output .= "vrrp_sync_group $sync_group \{\n\tgroup \{\n";
	foreach my $vrrp_instance ( 0 .. $#{ $HoA_sync_groups{$sync_group} } ) {
	    $output .= "\t\t$HoA_sync_groups{$sync_group}[$vrrp_instance]\n";
	}
	$output .= "\t\}\n\}\n";
    }
    return $output;
}

sub vrrp_read_changes {
    my @lines = ();
    open(my $FILE, "<", $changes_file) or die "Error: read $!";
    @lines = <$FILE>;
    close($FILE);
    chomp @lines;
    return @lines;
}

sub vrrp_save_changes {
    my @list = @_;

    my $num_changes = scalar(@list);
    vrrp_log("saving changes file $num_changes");
    open(my $FILE, ">", $changes_file) or die "Error: write $!";
    print $FILE join("\n", @list), "\n";
    close($FILE);
}

sub vrrp_find_changes {

    my @list = ();
    my $config = new Vyatta::Config;
    my $vrrp_instances = 0;

    foreach my $type (("ethernet", "bonding")) {

	$config->setLevel("interfaces $type");
	my @eths = $config->listNodes();
	foreach my $eth (@eths) {
	    my $path = "interfaces $type $eth";
	    $config->setLevel($path);
	    if ($config->exists("vrrp")) {
		my %vrrp_status_hash = $config->listNodeStatus("vrrp");
		my ($vrrp, $vrrp_status) = each(%vrrp_status_hash);
		if ($vrrp_status ne "static") {
		    push @list, $eth;
		    vrrp_log("$vrrp_status found $eth");
		}
	    }
	    if ($config->exists("vif")) {
	 	my $path = "interfaces $type $eth vif";
		$config->setLevel($path);
		my @vifs = $config->listNodes();
		foreach my $vif (@vifs) {	
		    my $vif_intf = $eth . "." . $vif;
	    	    my $vif_path = "$path $vif";
		    $config->setLevel($vif_path);
		    if ($config->exists("vrrp")) {
			my %vrrp_status_hash = $config->listNodeStatus("vrrp");
			my ($vrrp, $vrrp_status) = each(%vrrp_status_hash);
			if ($vrrp_status ne "static") {
			    push @list, "$eth.$vif";
			    vrrp_log("$vrrp_status found $eth.$vif");
			}
		    }
		}
	    }
	}

	#
	# Now look for deleted from the origin tree
	#
	$config->setLevel("interfaces $type");
	@eths = $config->listOrigNodes();
	foreach my $eth (@eths) {
	    my $path = "interfaces $type $eth";
	    $config->setLevel($path);
	    if ($config->isDeleted("vrrp")) {
		push @list, $eth;
		vrrp_log("Delete found $eth");
	    }
	    $config->setLevel("$path vif");
	    my @vifs = $config->listOrigNodes();
	    foreach my $vif (@vifs) {	
		my $vif_intf = $eth . "." . $vif;
		my $vif_path = "$path vif $vif";
		$config->setLevel($vif_path);
		if ($config->isDeleted("vrrp")) {
		    push @list, "$eth.$vif";
		    vrrp_log("Delete found $eth.$vif");
		} 
	    }
	}
    }

    my $num = scalar(@list);
    vrrp_log("Start transation: $num changes");
    if ($num) {
	vrrp_save_changes(@list);
    }
    return $num;
}

sub remove_from_changes {
    my $intf = shift;

    my @lines = vrrp_read_changes();
    if (scalar(@lines) < 1) {
	#
	# we shouldn't get to this point, but try to handle it if we do
	#
	vrrp_log("unexpected remove_from_changes()");
	system("rm -f $changes_file");
	return 0;
    }
    my @new_lines = ();
    foreach my $line (@lines) {
	if ($line =~ /$intf$/) {
	    vrrp_log("remove_from_changes [$line]");
	} else {
	    push @new_lines, $line;
	}
    }

    my $num_changes = scalar(@new_lines);
    if ($num_changes > 0) {
	vrrp_save_changes(@new_lines);
    } else {
	system("rm -f $changes_file");
    }
    return $num_changes;
}

sub vrrp_update_config {
    my ($intf) = @_;

    my @errs   = ();
    my $date   = localtime();
    my $output = "#\n# autogenerated by $0 on $date\n#\n\n";

    my $config = new Vyatta::Config;
    my $vrrp_instances = 0;

    for my $type (("ethernet", "bonding")) {

	$config->setLevel("interfaces $type");
	my @eths = $config->listNodes();
	foreach my $eth (@eths) {
	    my $path = "interfaces $type $eth";
	    $config->setLevel($path);
	    if ($config->exists("vrrp")) {
		my ($inst_output, @inst_errs) =
		    keepalived_get_values($eth, $path);
		if (scalar(@inst_errs)) {
		    push @errs, @inst_errs;
		} else {
		    $output .= $inst_output;
		    $vrrp_instances++;
		}
	    }
	    if ($config->exists("vif")) {
		my $path = "interfaces $type $eth vif";
		$config->setLevel($path);
		my @vifs = $config->listNodes();
		foreach my $vif (@vifs) {
		    my $vif_path = "$path $vif";
		    $config->setLevel($vif_path);
		    if ($config->exists("vrrp")) {
			#
			# keepalived gets real grumpy with interfaces that
			# don't exist, so skip vlans that haven't been
			# instantiated yet (typically occurs at boot up).
			#
			my $vif_intf = $eth . "." . $vif;
			if (!(-d "/sys/class/net/$vif_intf")) {
			    push @errs, "vlan doesn't exist $vif_intf";
			    next;
			}
			my ($inst_output, @inst_errs) = 
			    keepalived_get_values($vif_intf, $vif_path);
			if (scalar(@inst_errs)) {
			    push @errs, @inst_errs;
			} else {
			    $output .= $inst_output;
			    $vrrp_instances++;
			}
		    }
		}
	    }
	}
    }
      
    if ($vrrp_instances > 0) {
	my $sync_groups = vrrp_get_sync_groups();
	if (defined $sync_groups && $sync_groups ne "") {
	    $output = $sync_groups . $output;
	}
	keepalived_write_file($conf_file, $output);
    } 
    return ($vrrp_instances, @errs);
}

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

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

sub list_vrrp_intf {
    my $config = new Vyatta::Config;
    my @intfs = ();

    foreach my $type (("ethernet", "bonding")) {

	$config->setLevel("interfaces $type");
	my @eths = $config->listOrigNodes();
	foreach my $eth (@eths) {
	    my $path = "interfaces $type $eth";
	    $config->setLevel($path);
	    push @intfs, $eth if $config->existsOrig("vrrp");
	    if ($config->existsOrig("vif")) {
		my $path = "interfaces $type $eth vif";
		$config->setLevel($path);
		my @vifs = $config->listOrigNodes();
		foreach my $vif (@vifs) {	
		    my $vif_intf = $eth . "." . $vif;
	    	    my $vif_path = "$path $vif";
		    $config->setLevel($vif_path);
		    push @intfs, $vif_intf if $config->existsOrig("vrrp");
		}
	    }
	}
    }
    return @intfs;
}

sub list_vrrp_group {
    my ($name) = @_;
    my $config = new Vyatta::Config;
    my $path;
    if ($name =~ /bond/) {
	$path = "interfaces bonding $name";
	if ($name =~ /(bond\d+)\.(\d+)/) {
	    $path = "interfaces bonding $1 vif $2"; 
	}
    } else {
	$path   = "interfaces ethernet $name";
	if ($name =~ /(eth\d+)\.(\d+)/) {
	    $path = "interfaces ethernet $1 vif $2"; 
	}
    }
    $path .= " vrrp vrrp-group";
    $config->setLevel($path);
    my @groups = $config->listOrigNodes();
    return @groups;
}


#
# main
#
GetOptions("vrrp-action=s" => \$action,
	   "intf=s"        => \$vrrp_intf,
	   "group=s"       => \$vrrp_group,
           "vip=s"         => \$vrrp_vip);

if (! defined $action) {
    print "no action\n";
    exit 1;
}

if ($action eq "update") {
    $changes_file = get_changes_file();
    $conf_file    = get_conf_file();
    vrrp_log("vrrp update $vrrp_intf");
    if ( ! -e $changes_file) {
	my $num_changes = vrrp_find_changes();
	if ($num_changes == 0) {
	    #
	    # Shouldn't happen, but ...
	    #
	    vrrp_log("unexpected 0 changes");	    
	}
    }
    my ($vrrp_instances, @errs) = vrrp_update_config($vrrp_intf);
    my $more_changes = remove_from_changes($vrrp_intf);
    vrrp_log(" instances $vrrp_instances, $more_changes");
    if ($vrrp_instances > 0 and $more_changes == 0) {
	restart_daemon($conf_file);
    } 
    if ($vrrp_instances == 0) {
	stop_daemon();
	system("rm -f $conf_file");
    }
    if (scalar(@errs)) {
	print join("\n", @errs);
	vrrp_log(join("\n", @errs));
	exit 1
    }
    exit 0;
}

if ($action eq "delete") {
    if (! defined $vrrp_intf || ! defined $vrrp_group) {
	print "must include interface & group";
	exit 1;
    }
    vrrp_log("vrrp delete $vrrp_intf $vrrp_group");
    my $state_file = get_state_file($vrrp_intf, $vrrp_group);
    system("rm -f $state_file");
    exit 0;
}

if ($action eq "check-vip") {
    if (! defined $vrrp_vip) {
	print "must include the virtual-address to check";
	exit 1;
    }
    my $rc = 1;
    if ($vrrp_vip =~ /\//) {
	$rc = Vyatta::TypeChecker::validateType('ipv4net', $vrrp_vip, 1);
    } else {
	$rc = Vyatta::TypeChecker::validateType('ipv4', $vrrp_vip, 1);
    }
    exit 1 if ! $rc;
    exit 0;
}

if ($action eq "list-vrrp-intf") {
    my @intfs = list_vrrp_intf();
    print join(' ', @intfs);
    exit 0;
}

if ($action eq "list-vrrp-group") {
    if (! defined $vrrp_intf) {
	print "must include interface\n";
	exit 1;
    }
    my @groups = list_vrrp_group($vrrp_intf);
    print join(' ', @groups);
    exit 0;
}

exit 0;

# end of file