#! /usr/bin/perl

# 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.

use strict;
use lib "/opt/vyatta/share/perl5/";
use XorpConfigParser;
use Vyatta::Config;
use Sys::Syslog qw(:standard :macros);
use Fcntl qw(:flock);

my $BOOTFILE    = "/opt/vyatta/etc/config/config.boot";
my $VYATTACFG   = "/opt/vyatta/config/active";

my $UDEVDIR     = "/dev/.udev/";
my $VYATTAUDEV	= $UDEVDIR . "vyatta";
my $LOCKFILE	= $UDEVDIR . ".vyatta-lock";
my $UDEVLOG	= $UDEVDIR . "log/";
my $LOGFILE     = $UDEVLOG . "vyatta-net-name";

# Check if interface name is free to use
sub is_available {
    my ($interfaces, $ifname) = @_;

    my $count = grep { $_ eq $ifname } values %$interfaces;
    return ($count == 0);
}

# Find next available interface name
sub find_available {
    my ($interfaces, $ifprefix) = @_;
    $ifprefix =~ s/\d+$//;

    for (my $id = 0; ; $id++) {
	my $ifname = sprintf("%s%d", $ifprefix, $id);

	# is it in Vyatta config?
	return $ifname if (is_available($interfaces, $ifname));
    }
}

# Find the hardware id in the parsed config node for interface
sub get_hwid_from_children {
    my $children = shift;

    foreach my $attr (@$children) {
	next unless ($attr->{'name'} =~ /^hw-id ([0-9a-f:]+)/);
	return $1;
    }

    return;	# not found
}


# Leave file for vyatta_interface_rescan
sub leave_rescan_hint {
    my ($ifname, $hwaddr) = @_;
    my $name = "$VYATTAUDEV/$ifname";

    mkdir($VYATTAUDEV);
    open (my $f, '>', $name)
	or die "Can't create $name : $!";

    print {$f} "$hwaddr\n";
    close $f;
    return 1;
}

# Use biosdevname program (ethernet only)
# to try and find name based on PCI slot and DMI info
sub biosdevname {
    my $ifname = shift;

    # biosdevname works only on ethernet devices
    return $ifname unless ($ifname =~ /^eth/);

    my $biosname = `/sbin/biosdevname --policy all_ethN -i $ifname 2>>$UDEVLOG/biosdevname`;
    chomp $biosname;

    # if biosdevname has no answer it outputs a nothing
    return ($biosname eq '') ? $ifname : $biosname;
}

# parse vyatta config.boot
# if file does not then running before off livecd then return empty hash
sub parse_config_boot {
    my $interfaces = {};

    if ( -f $BOOTFILE ) {
	my $xcp = new XorpConfigParser();
	$xcp->parse($BOOTFILE);

	my $inode = $xcp->get_node(['interfaces']);
	if ($inode) {
	    foreach my $child (@{$inode->{'children'}}) {
		my $name = $child->{'name'};
		next unless ($name =~ /^ethernet (.*)|^wireless (.*)/);

		my $intf = $1;
		my $hwid = get_hwid_from_children($child->{'children'});
		next unless $hwid;
	    
		$interfaces->{$hwid} = $intf;
	    }
	}
    }

    return $interfaces;
}

# Determine network name to use based on Vyatta config during boot
sub coldplug {
    my ($ifname, $hwaddr) = @_;

    # at this time root directory is read-only so use log file instead
    mkdir ($UDEVLOG);
    open (my $log, '>>', $LOGFILE)
	or die "Can't open $LOGFILE : $!";
    print {$log} "lookup $ifname $hwaddr\n";

    # parse config file to produce map of existing hw-id values
    my $interfaces = parse_config_boot();

    # is name already in config file
    my $newname = $interfaces->{$hwaddr};
    if ($newname) {
	print {$log} "use hw-id $hwaddr in config mapped to '$newname'\n";
	return $newname;
    }

    # add already assigned names
    if (opendir(my $dir, $VYATTAUDEV)) {
	foreach my $intf (grep { ! /^\./ } readdir($dir)) {
	    if (open (my $f, '<', "$VYATTAUDEV/$intf")) {
		my $hwid = <$f>;
		close $f;
		chomp $hwid;

		$interfaces->{$hwid} = $intf;
	    }
	}
    }

    $newname = biosdevname($ifname);
    print {$log} "biosdevname for $ifname returned '$newname'\n";

    unless (is_available($interfaces, $newname)) {
	$newname = find_available($interfaces, $newname);
    }

    print {$log} "new name for '$ifname' is '$newname'\n", $ifname, $newname;
    close $log;

    leave_rescan_hint($newname, $hwaddr);

    return $newname;
}

# Determine name from active config
sub hotplug {
    my ($ifname, $hwaddr) = @_;

    # real filesystem available use real logging
    openlog("vyatta-net-name", "", LOG_DAEMON);

    # Parse active config
    my $cfg = new Vyatta::Config;
    $cfg->setLevel('interfaces');

    my $interfaces = {};
    foreach my $type ($cfg->listOrigNodes()) {
	next unless ($type eq 'ethernet') || ($type eq 'wireless');
	foreach my $intf ($cfg->listOrigNodes($type)) {
	    my $hwid = $cfg->returnOrigValue("$type $intf hw-id");
	    next unless $hwid;
	    # TBD this could be a hash with name and path?
	    $interfaces->{$hwid} = $intf;
	}
    }

    my $newname = $interfaces->{$hwaddr};
    if ($newname) {
	syslog(LOG_DEBUG, "use hw-id %s in config mapped to '%s'", $hwaddr, $newname);
	return $newname;
    }

    $newname = biosdevname($ifname);
    syslog(LOG_DEBUG, "biosdevname for %s returned '%s'", $ifname, $newname);

    unless (is_available($interfaces, $newname)) {
	$newname = find_available($interfaces, $newname);
    }

    syslog(LOG_INFO, "new name for '%s' is '%s'", $ifname, $newname);

    return $newname;
}

my $LOCKF;
sub lock_file {
    open ($LOCKF, '>', $LOCKFILE)
	or die "Can't open $LOCKFILE : $!";

    flock ($LOCKF, LOCK_EX)
	or die "Can't lock $LOCKFILE : $!";

}

sub unlock_file {
    close $LOCKF;
    $LOCKF = undef;
}

# This script is called from udev with two arguments
# it outputs the new name (if any) to stdout
if ($#ARGV != 1) {
    die "vyatta_net_name called with wrong args(%d) : %s",
	   $#ARGV, join(' ', @ARGV);
}

my $ifname = $ARGV[0];
my $hwaddr = $ARGV[1];

lock_file;
my $newname;
if ( -d $VYATTACFG ) {
    $newname = hotplug($ifname, $hwaddr);
} else {
    $newname = coldplug($ifname, $hwaddr);
}
unlock_file;

print "$newname\n" if ($newname);

exit 0;