#!/usr/bin/perl
#
# Module: vyatta-interfaces.pl
# 
# **** License ****
# Version: VPL 1.0
# 
# The contents of this file are subject to the Vyatta Public License
# Version 1.0 ("License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.vyatta.com/vpl
# 
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
# the License for the specific language governing rights and limitations
# under the License.
# 
# 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 VyattaConfig;
use VyattaMisc;
use Getopt::Long;

use NetAddr::IP;

use strict;
use warnings;

my $dhcp_daemon = '/sbin/dhclient';
my $dhcp_conf   = '/etc/dhcp3/dhclient.conf';
my $dhcp_pid    = '/var/run/dhclient.pid';
my $dhcp_leases = '/var/lib/dhcp3/dhclient.leases';


my ($eth_update, $eth_delete, $addr, $restart_dhclient, $dev);
GetOptions("eth-addr-update=s" => \$eth_update,
	   "eth-addr-delete=s" => \$eth_delete,
	   "valid-addr=s"      => \$addr,
	   "restart-dhclient!" => \$restart_dhclient,
           "dev=s"             => \$dev,
);

if (defined $eth_update)       { update_eth_addrs($eth_update, $dev); }
if (defined $eth_delete)       { delete_eth_addrs($eth_delete, $dev);  }
if (defined $addr)             { is_valid_addr($addr, $dev); }
if (defined $restart_dhclient) { dhcp_restart_daemon(); }

sub is_ip_configured {
    my ($intf, $ip) = @_;
    my $wc = `ip addr show $intf | grep $ip | wc -l`;
    if (defined $wc and $wc > 0) {
	return 1;
    } else {
	return 0;
    }
}

sub is_ip_duplicate {
    my ($intf, $ip) = @_;

    # 
    # get a list of all ipv4 and ipv6 addresses
    #
    my @ipaddrs = `ip addr show | grep inet | cut -d" " -f6`;
    chomp @ipaddrs;
    my %ipaddrs_hash = map { $_ => 1 } @ipaddrs;

    if (defined $ipaddrs_hash{$ip}) {
	#
	# allow dup if it's the same interface
	#
	if (is_ip_configured($intf, $ip)) {
	    return 0;
	}
	return 1;
    } else {
	return 0;
    }
}

sub is_dhcp_running {
    if (-f $dhcp_pid) {
	my $pid = `cat $dhcp_pid`;
	chomp $pid;
	my $ps = `ps -p $pid -o comm=`;

	if (defined($ps) && $ps ne "") {
	    return 1;
	} 
    }
    return 0;
}

sub dhcp_start_daemon {
    my $cmd = "$dhcp_daemon -q -nw &";
    system($cmd);
}

sub dhcp_stop_daemon {
    if (is_dhcp_running()) {
	my $pid = `cat $dhcp_pid`;
	system("kill $pid");
    } 
    system("rm -f $dhcp_pid");
}

sub dhcp_restart_daemon {
    #
    # check if vyatta has generated the config file, otherwise
    # an empty config will try to get new addresses for all
    # interfaces
    #
    my $grep = `grep vyatta-interfaces.pl $dhcp_conf | wc -l`;
    chomp $grep;
    if (!defined $grep or $grep != 1) {
	die "DHCP client not configured\n";
    }
    if (is_dhcp_running()) {
	dhcp_stop_daemon();
    }
    dhcp_start_daemon();	
}

sub dhcp_release_addr {
    my $intf = shift;
    my $cmd = "$dhcp_daemon -q -r $intf 2> /dev/null";
    system($cmd);
}

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";
    $output .= "request subnet-mask, broadcast-address, time-offset, routers,\n";
    $output .= "\tdomain-name, domain-name-servers, host-name,\n";
    $output .= "\tinterface-mtu;\n\n";
    return $output;
}

sub dhcp_get_interfaces {
    my @dhcp_intfs;

    my $config = new VyattaConfig;

    $config->setLevel("interfaces ethernet");
    my @eths = $config->listNodes();
    foreach my $eth (@eths) {
	$config->setLevel("interfaces ethernet $eth");
	if ($config->exists("address")) {
	    my @addrs = $config->returnValues("address");
	    foreach my $addr (@addrs) {
		if (defined $addr && $addr eq "dhcp") {
		    push @dhcp_intfs, $eth;
		}
	    }
	}
	$config->setLevel("interfaces ethernet $eth vif");
	my @vifs = $config->listNodes();
	foreach my $vif (@vifs) {
	    $config->setLevel("interfaces ethernet $eth vif $vif");
	    my @addrs = $config->returnValues("address");
	    foreach my $addr (@addrs) {
		if (defined $addr && $addr eq "dhcp") {
		    push @dhcp_intfs, "$eth.$vif";
		}
	    }
	}
    }
    return @dhcp_intfs;
}

sub is_dhcp_enabled {
    my $intf = shift;

    my $config = new VyattaConfig;

    if ($intf =~ m/(\w+)\.(\d)/) {
	$config->setLevel("interfaces ethernet $1 vif $2");
    } else {
	$config->setLevel("interfaces ethernet $intf");
    }
    my @addrs = $config->returnOrigValues("address");
    foreach my $addr (@addrs) {
	if (defined $addr && $addr eq "dhcp") {
	    return 1;
	}
    }
    return 0;
}

sub is_address_enabled {
    my $intf = shift;

    my $config = new VyattaConfig;

    if ($intf =~ m/(\w+)\.(\d)/) {
	$config->setLevel("interfaces ethernet $1 vif $2");
    } else {
	$config->setLevel("interfaces ethernet $intf");
    }
    my @addrs = $config->returnOrigValues("address");
    foreach my $addr (@addrs) {
	if (defined $addr && $addr ne "dhcp") {
	    return 1;
	}
    }
    return 0;
}

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

sub dhcp_update_config {
    my $output = dhcp_conf_header();
    my $hostname = get_hostname();

    my $config = new VyattaConfig;
    my $dhcp_instances = 0;
    my @dhcp_intfs = dhcp_get_interfaces();
    foreach my $intf (@dhcp_intfs) {
	$output .= "interface \"$intf\" {\n";
	if (defined($hostname)) {
	    $output .= "\tsend host-name \"$hostname\";\n";
	}
        $output .= "}\n\n";
	$dhcp_instances++;
    }

    if ($dhcp_instances > 0) {
	my $conf_file = $dhcp_conf;
	dhcp_write_file($conf_file, $output);
	dhcp_restart_daemon();
    }
    return $dhcp_instances;
}

sub update_dhcp_client {
    my $dhcp_instances = dhcp_update_config();
    if ($dhcp_instances == 0) {
	dhcp_stop_daemon();
    }
}

sub is_ip_v4_or_v6 {
    my $addr = shift;

    my $ip = NetAddr::IP->new($addr);
    if (defined $ip && $ip->version() == 4) {
	#
	# the call to IP->new() will accept 1.1 and consider
        # it to be 1.1.0.0, so add a check to force all
	# 4 octets to be defined
        #
	if ($addr !~ /\d+\.\d+\.\d+\.\d+/) {
	    return undef;
	}
	return 4;
    }
    $ip = NetAddr::IP->new6($addr);
    if (defined $ip && $ip->version() == 6) {
	return 6;
    }
    
    return undef;
}

sub update_eth_addrs {
    my ($addr, $intf) = @_;

    if ($addr eq "dhcp") {
	update_dhcp_client();
	return;
    } 
    my $version = is_ip_v4_or_v6($addr);
    if (!defined $version) {
	exit 1;
    }
    if (is_ip_configured($intf, $addr)) {
	#
	# treat this as informational, don't fail
	#
	print "Address $addr already configured on $intf\n";
	exit 0;
    }

    if ($version == 4) {
	return system("ip addr add $addr broadcast + dev $intf");
    }
    if ($version == 6) {
	return system("ip -6 addr add $addr dev $intf");
    }
    print "Error: Invalid address/prefix [$addr] for interface $intf\n";
    exit 1;
}

sub delete_eth_addrs {
    my ($addr, $intf) = @_;

    if ($addr eq "dhcp") {
	dhcp_release_addr($intf);
	update_dhcp_client();
	system("rm -f /var/lib/dhcp3/dhclient_$intf\_lease");
	return;
    } 
    my $version = is_ip_v4_or_v6($addr);
    if (!defined $version) {
	exit 1;
    }
    if ($version == 4) {
	return system("ip addr del $addr dev $intf");
    }
    if ($version == 6) {
	return system("ip -6 addr del $addr dev $intf");
    }
}

sub is_valid_addr {
    my ($addr_net, $intf) = @_;

    if ($addr_net eq "dhcp") { 
	if ($intf eq "lo") {
	    print "Error: can't use dhcp client on loopback interface\n";
	    exit 1;
	}
	if (is_dhcp_enabled($intf)) {
	    print "Error: dhcp already configured for $intf\n";
	    exit 1;
	}
	if (is_address_enabled($intf)) {
	    print "Error: remove static addresses before enabling dhcp for $intf\n";
	    exit 1;
	}
	exit 0; 
    }

    my ($addr, $net);
    if ($addr_net =~ m/^([0-9a-fA-F\.\:]+)\/(\d+)$/) {
	$addr = $1;
	$net  = $2;
    } else {
	exit 1;
    }

    my $version = is_ip_v4_or_v6($addr_net);
    if (!defined $version) {
	exit 1;
    }

    if (is_dhcp_enabled($intf)) {
	print "Error: remove dhcp before adding static addresses for $intf\n";
	exit 1;
    }
    if (is_ip_duplicate($intf, $addr_net)) {
	print "Error: duplicate address/prefix [$addr_net]\n";
	exit 1;
    }

    if ($version == 4) {
	if ($net > 0 && $net <= 32) {
	    exit 0;
	}
    } 
    if ($version == 6) {
	if ($net > 1 && $net <= 128) {
	    exit 0;
	}
    }

    exit 1;
}

exit 0;

# end of file