#!/bin/sh
#
# bondctl -- network bonding control program
#
# (c) 2009, Arthur Corliss <corliss@digitalmages.com>
#
# $Id: bondctl.in,v 1.8 2012/01/13 01:18:52 acorliss Exp $
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    any later version.
#
#    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.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#

IPROUTE=/sbin/ip
MODPROBE=/sbin/modprobe
SYSROOT=/sys/class/net
MASTERS=
DEVNAME=

VER=`grep '^# .Id: ' $0 | awk '{ print $4 }'`

warn () {
    echo "$@" 1>&2
}

die () {
    warn ERROR: $@
    exit 1
}

genDevName () {
    # This function generates a generic bond{n} name in first-available 
    # order.  The generated name is stored in the DEVNAME variable.
    #
    # Usage: genDevName

    local i=0
    while [ -e $SYSROOT/bond$i ]; do
      let i++
    done

    DEVNAME=bond$i
}

valDevName () {
    # Make sure a valid device name was passed.  This tests both the 
    # existance of the network device as well that it's listed as a 
    # bonding master.
    #
    # Usage: valDevName "bond0"

    local dev=$1
    local mdev

    if [ "$dev" == "" ]; then
        die "Missing device name"
    elif [ ! -e "$SYSROOT/$dev" ]; then

        # Try to load via modprobe alias
        $MODPROBE $dev
        sleep 3

        # Check again
        if [ ! -e "$SYSROOT/$dev" ]; then
            die "Device $dev does not exist"
        fi
    fi

    local found=0
    for mdev in $MASTERS ; do
        if [ "$mdev" == "$dev" ]; then
            found=1
            break
        fi
    done
    if [ ! $found ]; then
        die "Device $dev is not listed as a bonding master"
    fi
}

detail () {
    # Displays detailed information on the status and configuration 
    # of the bonding master.
    #
    # Usage: detail "bond0"

    local dev=$1
    valDevName $dev
    local state=`cat "$SYSROOT/$dev/operstate"`
    local slaves=`cat "$SYSROOT/$dev/bonding/slaves"`
    local actvslave=`cat "$SYSROOT/$dev/bonding/active_slave"`
    local mode=`cat "$SYSROOT/$dev/bonding/mode"`
    local monitor

    # See what type of monitoring is used and extract information
    if [ `cat "$SYSROOT/$dev/bonding/miimon"` != "0" ]; then
        monitor="MII: `cat $SYSROOT/$dev/bonding/miimon`msec"
    elif [ `cat "$SYSROOT/$dev/bonding/arp_interval"` != "0" ]; then
        local arpip=`cat "$SYSROOT/$dev/bonding/arp_ip_target"`
        monitor="ARP: $arpip, `cat $SYSROOT/$dev/bonding/arp_interval`msec"
    else
        monitor="No monitoring enabled"
    fi
    cat << EOF
Bonding Master: $dev
    Oper State:     $state
    Slaves:         $slaves
    Active Slave:   $actvslave
    Mode:           $mode
    Monitor:        $monitor
EOF
}

setOperState () {
    # Change bonding master's operating status to down along with all slaves.
    #
    # Usage: setOperState "bond0" "up"

    local dev=$1
    local state=$2
    local slave
    valDevName $dev

    # If we're bringing up the interfaces let's make sure we have a default
    # monitoring of miimon=100 if nothing is already set
    if [ `cat "$SYSROOT/$dev/bonding/miimon"` == "0" ]; then
        if [ `cat "$SYSROOT/$dev/bonding/arp_interval"` == "0" ]; then
            echo 100 > "$SYSROOT/$dev/bonding/miimon"
        fi
    fi

    # Change the master first, then the slaves
    $IPROUTE link set "$dev" $state
    if [ $? -ne 0 ]; then
        die "Failed to $state device $dev"
    fi
    for slave in `cat $SYSROOT/$dev/bonding/slaves` ; do
        $IPROUTE link set "$slave" $state
        if [ $? -ne 0 ]; then
            die "Failed to $state device $slave"
        fi
    done
}

addMaster () {
    # Add a new master bonding device
    #
    # Usage: addMaster "bond5"

    local dev=$1

    # Auto-gen a name if it was not specified
    if [ "$dev" == "" ]; then
        genDevName
        dev=$DEVNAME
    fi

    # Add the device
    if [ ! -e "$SYSROOT/$dev" ]; then
        echo "+$dev" > $SYSROOT/bonding_masters
        if [ $? -ne 0 ]; then
            die "Failed to add device $dev"
        fi
    fi
}

delMaster () {
    # Disables and removes a master bonding device
    #
    # Usage: delMaster "bond5"

    local dev=$1
    local slave

    valDevName $dev
    setOperState "$dev" down

    # Detach all slaves
    for slave in `cat "$SYSROOT/$dev/bonding/slaves"` ; do
        echo "-$slave" > "$SYSROOT/$dev/bonding/slaves"
        if [ $? -ne 0 ]; then
            die "Failed to detach slave device $slave"
        fi
    done

    # Remove the device
    if [ -e "$SYSROOT/$dev" ]; then
        echo "-$dev" > $SYSROOT/bonding_masters
        if [ $? -ne 0 ]; then
            die "Failed to remove device $dev"
        fi
    fi
}

attach () {
    # Attaches a slave to an existing bonding master
    #
    # Usage: attach "bond5" "eth0"

    local dev=$1
    local slave=$2
    local state
    local inetaddr
    local inet6addr
    local bondmac

    valDevName $dev

    # Preserve MAC address if one is already assigned
    bondmac=`$IPROUTE link show dev "$dev" | grep link/ether | \
        awk '{ print $2 }'`

    # Make sure proposed slave exists
    if [ ! -e "$SYSROOT/$slave/operstate" ]; then

        # Try to load by modprobe alias
        $MODPROBE $slave
        sleep 3

        # Check again
        if [ ! -e "$SYSROOT/$slave/operstate" ]; then
            die "Slave device $slave does not exist"
        fi
    fi

    # Make sure proposed slave is currently down
    state=`cat "$SYSROOT/$slave/operstate"`
    if [ "$state" == "up" ]; then

        # If it's up, let's make sure there's no interfaces assigned
        inetaddr=`$IPROUTE -4 addr show dev "$slave" | grep inet`
        inet6addr=`$IPROUTE -6 addr show dev "$slave" | \
            grep 'scope \(global\|site\)'`
        if ! ( test -z "$inetaddr" || test -z "$inet6addr" ); then

            # There's addresses assigned, so fail
            die "Slave device $slave is currently up"
        fi
    fi

    # Attach slave
    $IPROUTE link set down dev "$slave" && \
        $IPROUTE addr flush dev "$slave" && \
        echo "+$slave" > "$SYSROOT/$dev/bonding/slaves"
    if [ $? -ne 0 ]; then
        die "Failed to attach device $slave to $dev"
    fi

    # Reassign MAC
    if [ "$bondmac" != "00:00:00:00:00:00" ]; then
        $IPROUTE link set "$dev" address "$bondmac"
    fi

    # Bring the interface up if the master is already up
    state=`cat "$SYSROOT/$dev/operstate"`
    if [ "$state" != "down" ]; then
        $IPROUTE link set "$slave" up
    fi
}

detach () {
    # Detaches a slave from an existing bonding master
    #
    # Usage: detach "bond5" "eth0"

    local dev=$1
    local slave=$2
    local found
    local slaves
    local sdev

    valDevName $dev

    # Make sure slavedev is actually a slave for the passed master
    slaves=`cat "$SYSROOT/$dev/bonding/slaves"`
    found=0
    for sdev in $slaves ; do
        if [ "$slave" == "$sdev" ]; then
            found=1
            break
        fi
    done
    if [ ! $found ]; then
        die "Device $slave is not a slave for $dev"
    fi

    # Down device and detach slave
    ip link set "$slave" down && \
        echo "-$slave" > "$SYSROOT/$dev/bonding/slaves"
    if [ $? -ne 0 ]; then
        die "Failed to attach device $slave to $dev"
    fi
}

showParams () {
    # Displays a list of parameters in sysfs for bonding
    #
    # Usage: showParams "bond0"

    local dev=$1
    local param
    local params
    local value
    local c

    valDevName $dev

    echo "Parameters for $dev:"
    params=`ls "$SYSROOT/$dev/bonding/"`
    for param in $params ; do
        value=`cat "$SYSROOT/$dev/bonding/$param"`

        # Append spaces to parameter name for display purposes
        param="$param:"
        c=`echo $param | wc -c`
        while [ $c -lt 21 ]; do
            param="$param "
            c=`echo "$param" | wc -c`
        done
        echo "    $param$value"
    done
}

setParam () {
    # Sets the specified parameter to the desired value.
    #
    # Usage: setParam "bond0" "foo" "bar"

    local dev=$1
    local param=$2
    local value=$3

    valDevName $dev

    # Make sure desired parameter exists
    if [ ! -e "$SYSROOT/$dev/bonding/$param" ]; then
        die "Unknown parameter $param"
    fi

    # Apply the setting
    echo "$value" > "$SYSROOT/$dev/bonding/$param"
    if [ $? -ne 0 ]; then
        die "Failed to set $param for $dev"
    fi
}

assignMAC () {
    # Sets the master MAC address to the specified address
    #
    # Usage: assignMAC "bond0" "0e:c3:dd:a8:00:05"

    local dev=$1
    local mac=$2

    # Make sure the specified master exists
    valDevName $dev

    # Assign the MAC
    $IPROUTE link set "$dev" address "$mac"
    if [ $? -ne 0 ]; then
        die "Failed to set device $dev MAC to $mac"
    fi
}

assignIP () {
    # Sets the master IP address to the specified address
    #
    # Usage: assignIP "bond0" "192.168.0.5/14"

    local dev=$1
    local ipaddr=$2

    # Make sure the specified master exists
    valDevName $dev

    # Flush or assign IP
    if [ "$ipaddr" == "flush" ]; then
        $IPROUTE addr flush dev "$dev"
        if [ $? -ne 0 ]; then
            die "Failed to flush addresses from $dev"
        fi
    else
        $IPROUTE addr add "$ipaddr" dev "$dev"
        if [ $? -ne 0 ]; then
            die "Failed to set device $dev IP to $ipaddr"
        fi
    fi
}

runFile () {
    # Read the file and execute each line as a command.
    #
    # Usage: runFile foo.txt

    local file=$1
    local nlines
    local cargs
    local i

    if [ ! -r "$file" ]; then
        die "File $file not readable"
    fi

    nlines=`cat $file | wc -l`

    # Iterate over every line, ignoring comments and blank lines
    i=0
    while [ $i -lt $nlines ]; do
        let i++
        cargs=`sed -n ${i}p $file | sed 's/[[:space:]]*#.*//' | \
            sed 's/^[[:space:]]\+$//'`
        test ! -z "$cargs" && $0 $cargs
    done
}

help () {
    cat << EOF
bondctl, v$VER: (c) 2011, Arthur Corliss <corliss@digitalmages.com>
Usage: bondctl show
       bondctl detail {master|all}
       bondctl add {master}
       bondctl del {master}
       bondctl attach {master} {slave} [{slave2} ...]
       bondctl detach {master} {slave} [{slave2} ...]
       bondctl enable {master}
       bondctl disable {master}
       bondctl params {master}
       bondctl set {master} {param} {value}
       bondctl laddress {master} {MAC address}
       bondctl ipaddress {master} flush
       bondctl ipaddress {master} {IPv4/IPv6 CIDR address}
       bondctl help
       bondctl version
       bondctl -f {cmd file}
EOF
}

action () {
    # Choose what functions to perform based on first argument
    local action=$1
    local dev
    local slave
    case $action in
        show)
            for dev in $MASTERS ; do echo $dev ; done
            ;;
        detail)
            if [ "$2" == "all" ]; then
                for dev in $MASTERS ; do
                    detail $dev
                done
            else
                detail $2
            fi
            ;;
        add)
            addMaster $2
            ;;
        del)
            delMaster $2
            ;;
        attach)
            dev=$2
            shift 2
            for slave in $@ ; do
                attach $dev $slave
            done
            ;;
        detach)
            dev=$2
            shift 2
            for slave in $@ ; do
                detach $dev $slave
            done
            ;;
        enable)
            valDevName $2
            setOperState $2 up
            ;;
        disable)
            valDevName $2
            setOperState $2 down
            ;;
        laddress)
            assignMAC $2 $3
            ;;
        ipaddress)
            assignIP $2 $3
            ;;
        params)
            showParams $2
            ;;
        set)
            setParam $2 $3 $4
            ;;
        --help|help)
            help
            ;;
        --version|version)
            echo "$VER"
            ;;
        -f)
            runFile $2
            ;;
        "")
            die "No action given"
            ;;
        *)
            die "Uknown action $action"
            ;;
    esac
}

# Print the help message if no arguments are given
if [ ${#@} == 0 ]; then
    help
    exit 0
fi

# Make sure bonding support is loaded
if [ ! -f $SYSROOT/bonding_masters ]; then

    # Attempt to load the module
    $MODPROBE bonding
    sleep 2

    # Check again
    if [ ! -f $SYSROOT/bonding_masters ]; then
        die "Bonding support is not loaded."
    fi
fi

MASTERS=`cat $SYSROOT/bonding_masters`

# Take appropriate action
action $@

exit 0
