#!/bin/sh
################################################################################
# Except for any bugs, it should be able to cope with the following scenarios:
# 
# 1) One NIC, no kernel parameters:
#    It'll use DHCP.
# 2) One NIC, static ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>:
#    It'll use the static IP.
# 3) One NIC, IPAPPEND 1 or 3 or ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:
#    It'll use the static IP.
# 4) One NIC, incomplete ip=:<server-ip>:::<hostname>:<device>::
#    It'll use DHCP but override server-ip and hostname with the static ones.
# 5) One NIC, IPAPPEND 1 or 3 and autoconf=dhcp:
#    It'll use DHCP (requesting the provided ip but accepting whatever the DHCP
#    server provides) but override server-ip and hostname with the static ones.
#
# 6) Two or more NICs, no kernel parameters:
#    It'll use DHCP on all NICs, and use the first one that got a lease.
# 7) Two or more NICs, IPAPPEND 2 or BOOTIF=<01-mac-address> or <device>:
#    It'll use DHCP on the specified NIC.
# 8) Two or more NICs, IPAPPEND 3 or static ip along with BOOTIF or <device>:
#    It'll use the static IP on the specified device.
# 9) Two or more NICs, incomplete ip=:<server-ip>:::<hostname>:<device>::
#    It'll use DHCP on the specified device but override server-ip and hostname with the static ones.
#10) Two or more NICs, IPAPPEND 3 and autoconf=dhcp:
#    It'll use DHCP on the specified NIC and override server-ip and hostname with the static ones.
################################################################################

PREREQ=""

prereqs()
{
    echo "$PREREQ"
}

case $1 in
# get pre-requisites
prereqs)
    prereqs
    exit 0
    ;;
esac

# Exit if an LTSP boot was not requested
grep -Eqsw "init=/sbin/init-ltsp|ltsp" /proc/cmdline || exit 0

bring_up_interfaces()
{
    local i

    # Wait for the network interfaces to become available
    i=0
    while i=$(($i+1)); do
        interfaces=$(ip -oneline link show | sed -n '/ether/s/[0-9 :]*\([^:]*\).*/\1/p')
        if [ -n "$interfaces" ]; then
            break
        elif [ $i -ge 10 ]; then
            # After a while, give a shell to the user in case he can fix it
            panic "No network interfaces found"
            i=0
        else
            sleep 1
        fi
    done

    # Bring up the interface(s). $interface is set with BOOTIF.
    for i in ${interface:-$interfaces}; do
        ip link set dev $i up
    done

    # Wait for a network interface to be up
    i=0
    while i=$(($i+1)); do
        if ip -oneline link show up | grep -vw lo | grep -q LOWER_UP; then
            break
        elif [ $i -ge 4 ]; then
            # After a while, give a shell to the user in case he can fix it
            panic "No network interfaces are up"
            i=0
        else
            sleep 1
        fi
    done
}

# Analyze whatever information was provided in the kernel command line
process_kernel_parameters()
{
# "BOOTIF" can be passed manually or by IPAPPEND 2/3
# Form: BOOTIF=01-1a-2b-3c-4d-5e-6f, where 01=the ARP type code for ethernet
# http://syslinux.zytor.com/wiki/index.php/SYSLINUX#IPAPPEND_flag_val_.5BPXELINUX_only.5D
if [ -n "$BOOTIF" ]; then
    mac=$(echo $BOOTIF | sed -n 's/..-\(.*\)/\1/;y/ABCDEF-/abcdef:/p')
    interface=$(ip -oneline link show | sed -n "/ether $mac"'/{s/[0-9 :]*\([^:]*\).*/\1/p;q};')
fi

# "IP" can be passed manually or by IPAPPEND 1/3
# Form: ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>
# http://www.kernel.org/doc/Documentation/filesystems/nfsroot.txt
# Older initramfs-tools set "ip", newer set "IP".
IP=${IP:-$ip}
case "$IP" in
    "")
        ;;
    # If the user just specified e.g. ip=dhcp
    off|static|none|rarp|bootp|dhcp|both|all)
        autoconf="$IP"
        ;;
    *)
        local oldifs="${IFS-not set}"
        IFS=':'
        read ip tftp router subnet hostname new_interface new_autoconf<<EOF
$IP
EOF
        test "$oldifs" = "not set" && unset IFS || IFS="$oldifs"
        # Prefer the interface specified by BOOTIF
        interface=${interface:-$new_interface}
        # Provide a way to use both IPAPPEND 3 *and* autoconf=dhcp :-)
        autoconf=${autoconf:-$new_autoconf}
esac
}

# Returns true if there's not enough static boot information
dhcp_needed()
{
case "$autoconf" in
    off|static|none|"")
        # If any of the vars below is empty, we can't use a static ip
        [ -z "$ip" -o -z "$tftp" -o -z "$subnet" ]    # This is the return value
        ;;
    rarp|bootp|dhcp|both|all|*)
        return 0;
        ;;
esac
}

# Generate the dhcp script, /tmp/dhcp-script.sh
# udhcpc will be calling it on DHCP events
# When called for state="bound", dhcp-script.sh will generate dhcp-info.conf
# Then, dhcp-info.conf will be sourced
# TODO: what happens when the lease expires and the /tmp/dhcp-script.sh script will be called again?
# I think it won't be there, resulting in an error. Not that it matters so much... :-)
# Another way, instead of using /tmp/dhcp-script.sh, would be to:
# 1) copy the current script to /usr/share/initramfs-tools/script/udhcp while inside the initramfs (!),
# 2) run udhcpc -s /usr/share/initramfs-tools/script/udhcp,
# 3) use a "case 'prereqs' / 'bound' ..." to know if it's called from udhcp or not.
# This way no script would need to be generated, and udhcpc would be able to
# locate the current script even after the normal boot completes! :-)
generate_dhcp_script()
{
echo '#!/bin/sh

[ "$1" = "bound" ] || exit;

echo "# This file contains whatever information was provided by the DHCP server" > /tmp/dhcp-info.conf
for var in bootfile bootsize broadcast dns domain hostname ipttl lease lprsrv \
    message msstaticroutes mtu nisdomain nissrv ntpsrv rootpath router routes \
    search serverid sipsrv staticroutes subnet swapsrv tftp timezone wins wpad \
    boot_file interface ip mask siaddr; do
        eval value=\"\$$var\"
        if [ -n "$value" ]; then
            echo $var="\"$value\"" >> /tmp/dhcp-info.conf
        fi
done' > /tmp/dhcp-script.sh
chmod +x /tmp/dhcp-script.sh
}

do_dhcp()
{
hostname_param=${hostname:+"-h $hostname"}
ip_param=${ip:+"-r $ip"}
# If a specific interface wasn't provided, try all of them
# TODO: it would be nice if we could prefer the interface with the connected cable, though
interfaces=${interface:-$interfaces}

# Clear screen when quiet to dampen some noise
[ "$quiet" = "y" ] && clear

[ "$quiet" != "y" ] && echo "DHCP request for $i..."
while [ -z "$configured" ]; do
    for i in $interfaces; do    # Make a DHCP request for each interface
        if udhcpc -n -C -O rootpath -s /tmp/dhcp-script.sh -i $i $hostname_param $ip_param >/dev/null 2>&1; then
            configured="true"
            break
        fi
    done
done
[ "$quiet" != "y" ] && echo "Done."

# Source the generated dhcp-info.conf file which contains the dhcp variables.
# But assume that the command-line-provided server and hostname take precedence.
k_tftp="$tftp"
k_hostname="$hostname"
. /tmp/dhcp-info.conf
tftp=${k_tftp:-$tftp}
hostname=${k_hostname:-$hostname}
}

sanitize_configuration()
{
# $interface may not be defined if a static IP is desired; in this case, use the first one
# TODO: it would be good if we could prefer the interface with a connected cable...
# TODO: The $DEVICE that was set by initramfs.conf is not exported by init, is that by design?
interface=${interface:-$(ip -oneline link show | sed -n '/ether/{s/[0-9 :]*\([^:]*\).*/\1/p;q};')}

bootfile=${bootfile:-$boot_file}    # There are 2 bootfile options in DHCP
boot_file=$bootfile

tftp=${tftp:-$siaddr}
tftp=${tftp:-$sname}
tftp=${tftp:-$serverid}

# Ensure a default rootpath if it doesn't exist
if [ -z "$rootpath" ]; then
    if [ -f /conf/arch.conf ]; then
        rootpath=$(. /conf/arch.conf; echo "${DPKG_ARCH:+/opt/ltsp/$DPKG_ARCH}")
    fi
    rootpath=${rootpath:-/opt/ltsp/i386}
fi
}

# Converts a decimal subnet mask to the bit count needed for the CIDR notation
subnet_to_cidr()
{
local bits=32
local oldifs="${IFS-not set}"
IFS='.'
for byte in $1; do
    byte=$((255-$byte))
    while [ $byte -gt 0 ]; do
        bits=$(($bits-1))
        byte=$(($byte/2))
    done
done
test "$oldifs" = "not set" && unset IFS || IFS="$oldifs"
echo $bits
}

apply_configuration()
{
[ -z "$ip" -o -z "$tftp" -o -z "$subnet" ] && exit

ip address add $ip/$(subnet_to_cidr $subnet) broadcast ${broadcast:-+} dev $interface

for i in $router
do
    ip route add default via $i dev $interface
done

if [ -n "$hostname" ]; then
    echo "$hostname" > /proc/sys/kernel/hostname
fi

[ "$quiet" != "y" ] && echo "$interface configured at $ip:$tftp:$router:$subnet:$hostname"
}

export_configuration()
{
# Ensure that /run exists, otherwise older versions may fail.
mkdir -p /run

# dns and router may contain multiple values
read dns0 dns1 rest_dns <<EOF
$dns
EOF
read router0 rest_routers <<EOF
$router
EOF
echo "DEVICE='$interface'
IPV4ADDR='$ip'
IPV4BROADCAST='$broadcast'
IPV4NETMASK='$subnet'
IPV4GATEWAY='$router0'
IPV4DNS0='$dns0'
IPV4DNS1='$dns1'
HOSTNAME='$hostname'
DNSDOMAIN='$domain'
NISDOMAIN='$nisdomain'
ROOTSERVER='$tftp'
ROOTPATH='$rootpath'
filename='$bootfile'
DNS_SERVER='$dns'
SEARCH_DOMAIN='$search'
NTPSVR='$ntpsvr'
TIMESVR='$ntpsvr'
TIMEZONE='$timezone'
SWAPSVR='$swapsvr'" > /run/net-$interface.conf

# Also make a /tmp symlink for backwards compatibility
ln -sf /run/net-$interface.conf /tmp/net-$interface.conf
# Write the file that'll be sourced by the callee function run_scripts
mkdir -p /conf
# Only export the lines that have something after the =
sed -n "/[^=]*=[' ]*$/!s/.*/export &/p" "/run/net-$interface.conf" > /conf/param.conf
}

# Main
[ "$xtrace" = "udhcp" ] && set -x    # Provide a kernel parameter to enable tracing
. /scripts/functions
bring_up_interfaces
process_kernel_parameters
if dhcp_needed; then
    [ -x "/sbin/udhcpc" ] || exit    # But do handle any static IP requests
    generate_dhcp_script
    do_dhcp
fi
sanitize_configuration
apply_configuration
export_configuration
