#!/bin/sh
#/
#/ \file
#/
#/ \brief       configure network interfaces
#/
#/ \author      Arthur
#/
#/ \date        November 15, 2012
#/
#/ \version     0.2
#/


################################################################################
#                                                                              #
#                              C O N S T A N T S                               #
#                                                                              #
################################################################################


RC_NAME=`basename $0 2>/dev/null`
RC_DIR=`dirname \`readlink -qe $0\` 2>/dev/null`
RC_VER="0.2"
RC_ID="network control"

CONFIG_DIR="/etc/if.d"          # base directory for interface configurations
NETINFO_FILE="/proc/net/dev"


################################################################################
#                                                                              #
#                               I N C L U D E S                                #
#                                                                              #
################################################################################


. $RC_DIR/include/rc_utils


################################################################################
#                                                                              #
#                              F U N C T I O N S                               #
#                                                                              #
################################################################################


#-------------------------------------------------------------------------------


#/
#/ Output script usage and exit.
#/
#/ \returns
#/
#/ Always exit code 1.
#/
usage()
{
    $ECHO "Usage: $RC_NAME [-l][-n][-q][-v][-h][-V] [ACTION] [INTERFACE]..."

    exit 1
}


#-------------------------------------------------------------------------------


#/
#/ Output script version and exit.
#/
#/ \returns
#/
#/ Always exit code 1.
#/
version()
{
    $ECHO "$RC_NAME $RC_VER"

    exit 1
}


#-------------------------------------------------------------------------------


#/
#/ Bring up specified network interfaces. The configuration file of each
#/ interface determines exactly whether to bring up the interface and what
#/ settings to use for the interface. If an error occurs, the function
#/ aborts configuring the "failed" interface and continues with the next
#/ interface, if any.
#/
#/ Each interface can have a pre-start and post-start command associated with
#/ it. These commands start \em prior resp. \em after the interface is
#/ configured. Both pre-start and post-start commands must execute successfully
#/ (if they exist) else the current interface configuration is aborted and the
#/ configuration continues with the next interface.
#/
#/ \param[in] $1  List of interfaces (whitespace-separated).
#/
#/ \returns
#/
#/ Return code 0 in case no errors occurred while configuring the interfaces,
#/ otherwise code 1. Note that it's not an error if a config file says not to
#/ configure the interface.
#/
up()
{
    assert up "-n '$1'"

    ifs=$1          # interfaces to configure
    dev=""
    active=""
    ret=0
    err=0
    
    active=`list_active_ifs`
    
    $ECHO_N "Configuring network interface..."

    #
    # Starting configuring interfaces; abort and continue with the next one
    # in case an error occurred somewhere along the way.
    #
    for dev in $ifs
    do
        # skip interface if it's already active:
        if $ECHO $active | grep -q $dev 2>/dev/null
        then
            [ "$VERBOSE" = "1" ] && $ECHO "Skipping $dev; already active"
            
            continue
        fi

        read_config $dev
        if test $? -eq 0
        then
            if test "$ENABLE" = "1"
            then
                # pre-start command:
                do_commands $dev 0 START PRE 1
                ret=$?
                if test $ret -ne 0
                then
                    $ECHO
                    error "$dev: pre-start commands failed ($ret)"
                    err=1
                    continue
                fi
                
                configure_inet_interface $dev 1
                ret=$?
                if test $ret -eq 0
                then
                    sleep $START_SETTLE 2>/dev/null
                    $ECHO_N " $dev"
                else
                    [ "$VERBOSE" = "1" ] && $ECHO " [$dev failed ($ret)]"
                    err=1
                    continue
                fi

                # post-start command:
                do_commands $dev 0 START POST 1
                ret=$?
                if test $ret -ne 0
                then
                    $ECHO
                    error "$dev: post-start commands failed ($ret)"
                    err=1
                    continue
                fi
            else
                $ECHO_N "[$dev is disabled]"
            fi
        else
            $ECHO
            fatal "$dev: could not read configuration file"
        fi
    done
    $ECHO

    test $err -eq 0
    return $?
}


#-------------------------------------------------------------------------------


#/
#/ Bring down specified network interfaces. Same behavior but opposite effect
#/ as the up() function. Interfaces are disabled, or de-configured, and their
#/ associated pre- and post-stop commands (if defined) are execute accordingly.
#/ Contrary to the up() function, de-configuration of an interface is not
#/ aborted if its pre- or post-command fails. The function continues as much as
#/ possible to bring down the interface.
#/
#/ \param[in] $1  List of interfaces (whitespace-separated).
#/
#/ \returns
#/
#/ Return code 0 if network interfaces are down, otherwise 1.
#/
down()
{
    assert down "-n '$1'"

    ifs=$1
    dev=""
    active=""
    ret=0
    err=0
    
    # disable interfaces, if configured:
    active=`list_active_ifs`
    if test -n "$active"
    then
        $ECHO_N "Disabling network interface..."

        #
        # De-configuring interfaces; do not abort, but continue with the next
        # interface in case an error occurred somewhere along the way.
        #
        for dev in $ifs
        do
            # skip interface if it's not active:
            $ECHO $active | grep -q $dev 2>/dev/null
            if test $? -ne 0
            then
                [ "$VERBOSE" = "1" ] && $ECHO "Skipping $dev; already down"
                
                continue
            fi

            read_config $dev
            if test $? -eq 0
            then
                # pre-stop command:
                do_commands $dev 0 STOP PRE 0
                ret=$?
                if test $ret -ne 0
                then
                    $ECHO
                    warn "$dev: pre-stop commands failed ($ret)"
                    #err=1
                fi
                
                configure_inet_interface $dev 0
                ret=$?
                if test $ret -eq 0
                then
                    sleep $STOP_SETTLE 2>/dev/null
                    $ECHO_N " $dev"
                else
                    [ "$VERBOSE" = "1" ] && $ECHO " [$dev failed ($ret)]"
                    err=1
                fi
                
                # post-stop command:
                do_commands $dev 0 STOP POST 0
                ret=$?
                if test $ret -ne 0
                then
                    $ECHO
                    warn "$dev: post-stop commands failed ($ret)"
                    #err=1
                fi
            else
                $ECHO
                fatal "$dev: could not read configuration file"
            fi
        done
        $ECHO
    else
        $ECHO "Network subsystem not configured."
    fi

    test $err -eq 0
    return $?
}


#-------------------------------------------------------------------------------


#/
#/ Re-initialize network interfaces.
#/
#/ \param[in] $1  List of interfaces (whitespace-separated).
#/
#/ \returns
#/
#/ Return code 0 if re-initialization succeeded, otherwise 1.
#/
reinit()
{
    assert reinit "-n '$1'"

    ifs=$1
    dev=""

    for dev in $ifs
    do
        down "$ifs"  &&  sleep $RESTART_PAUSE 2>/dev/null  &&  up "$ifs"
        if test $? -eq 0
        then
            return 0
        else
            return 1
        fi
   done
}


#-------------------------------------------------------------------------------


#/
#/ Show operating state of network subsystem. Output which interfaces are
#/ currently active and verify whether active interfaces correspond to the
#/ predefined interfaces in the "interface config" directory (see
#/ \c CONFIG_DIR).
#/
#/ \param[in] $1  List of interfaces (whitespace-separated).
#/
#/ \returns
#/
#/ Always return code 0.
#/
status()
{
    assert status "-n '$1'"

    known=""        # interfaces known to the system
    defined=""      # interfaces defined in CONFIG_DIR
    undefined=""    # known interface but not defined in CONFIG_DIR
    active=""       # known interface and up
    inactive=""     # known interface but down
    ifs=$1
    dev=""
    state=""

    #
    # Output a list of interfaces known to the system, if there are any.
    # Note that when we're sorting a list we have to make sure each
    # device name is on a separate line since the `sort' command operates
    # on *lines* of text. To this end, we use the `fmt' command with an
    # impossible small line width, say 1, which will force each word (i.e.
    # device name) on a separate line.
    #
    active=`list_active_ifs`
    if test -n "$active"
    then
        known=`list_known_ifs`
        defined=`find_configs`

        #
        # We substract ("lexicographically") interface names from two
        # lists and this gives us a clue whether certain devices were
        # undefined or have become inactive.
        #
        undefined=`$ECHO $known $defined | fmt -w1 | sort | uniq -u | tr '\n' ' '`
        inactive=`$ECHO $known $active | fmt -w1 | sort | uniq -u | tr '\n' ' '`

        #
        # Check whether we're querying status for all defined interfaces
        # or just for specific ones. In the latter case, we'll output
        # limitted information about the specified interfaces while
        # outputting some additional general information when the status
        # of all interfaces is requested.
        # The list of interfaces passed to this function ($ifs) may or
        # may not correspond to all the defined interfaces ($defined).
        # There is no flag to indicate this, so we use a somewhat cumber-
        # some method to check the lists for equality: We combine both
        # lists and check for unique words. If there are none, status for
        # all interfaces is requested otherwise only specific interfaces.
        #
        if test -z "`$ECHO $ifs $defined | fmt -w1 | sort | uniq -u`"
        then
            $ECHO "Network subsystem active for: $active"

            #
            # Check for discrepancy between configured interfaces and
            # files in CONFIG_DIR.
            #
            if test -n "$undefined"
            then
                $ECHO "Undefined interfaces: $undefined"
            fi
            if test -n "$inactive"
            then
                $ECHO "In-active interfaces: $inactive"
            fi
        else
            for dev in $ifs
            do
                if $ECHO $known | grep -q $dev 2>/dev/null
                then
                    if $ECHO $defined | grep -q $dev 2>/dev/null
                    then
                        state="Defined"
                    else
                        state="Undefined"
                    fi

                    if $ECHO $active | grep -q $dev 2>/dev/null
                    then
                        state="Active, $state"
                    else
                        state="In-active, $state"
                    fi
                else
                    state="Unknown"
                fi
                $ECHO "Network interface \"$dev\": $state"
            done
        fi
    else
        $ECHO "Network subsystem down."
    fi

    return 0
}


#-------------------------------------------------------------------------------


#/
#/ Return list of interface configurations found in the "interface config"
#/ directory (see IF_BASEDIR constant). The interface config directory is
#/ not searched recursively, so any configs in sub-directories are ignored.
#/ Each filename in IF_BASEDIR corresponds to the name of a network
#/ interface device.
#/                      
#/ \returns         
#/              
#/ Whitespace-separated list of interface names (filename only) or
#/ empty string if none found.
#/              
list_defined_ifs()
{       
    ifcfgs=
            
    if test -d "$CONFIG_DIR"
    then
        ifcfgs=`find $CONFIG_DIR -maxdepth 1 -type f -printf '%P\n' 2>/dev/null | sort -g`

        $ECHO $ifcfgs
    else
        error "interface config directory not found: $CONFIG_DIR"
    fi
}
                                                    

#-------------------------------------------------------------------------------


#/
#/ Return list of known network interfaces. The interfaces returned by this
#/ function have been registered by the system, but they're not necessarily
#/ ready for communication purposes. In other words, a NIC's driver has been
#/ loaded, but the interface has not been assigned an address yet.
#/
#/ A whitespace-separated list of known interfaces is echo'ed to stdout or
#/ an empty string if none are available.
#/
list_known_ifs()
{
    ifs=""

    if test -r "$NETINFO_FILE"
    then
        # retrieve known/registered network devices from /proc filesystem:
        ifs=`cat $NETINFO_FILE | \
             sed -ne 's@^[ \t]*\([A-Za-z0-9]\+\):.*@\1@p' 2>/dev/null`

        $ECHO $ifs
    else
        error "network device info file not found: $NETINFO_FILE"
    fi
}


#-------------------------------------------------------------------------------


#/
#/ Return list of active network interfaces. An active interface is configured
#/ such that it's ready for communication purposes.
#/
#/ A whitespace-separated list of interfaces is echo'ed to stdout or an empty
#/ string if none are active.
#/
list_active_ifs()
{
    ifs=`netstat -i | tail -n +3 | cut -d' ' -f1`

    $ECHO $ifs
}


#-------------------------------------------------------------------------------


#/
#/ Show interfaces configured on the system. Only the interfaces defined in
#/ \c CONFIG_DIR are listed regardless of whether they are currently configured.
#/
#/ \returns
#/
#/ Does not return; exit code always 1.
#/
list_interfaces()
{
    cfgs=""
    dev=""
    id=""

    cfgs=`find_configs`
    for dev in $cfgs
    do
        read_config $dev
        
        id=$ID
        if test -z "$id"
        then
            id="<unknown>"
        fi

        $ECHO "$dev\t-- $id"
    done

    exit 1
}


#-------------------------------------------------------------------------------


#/
#/ Configure given Internet interface. The following settings are configured
#/ for the interface:
#/
#/ - IP address
#/ - Network mask
#/ - Gateway address
#/
#/ \param[in] $1  Interface device name to be configured.
#/
#/ \param[in] $2  Flag indicating whether to bring the interface up (1) or
#/                down (0).
#/
#/ \returns
#/
#/ Return code 0 if interface was successfully configured, otherwise code 1.
#/
configure_inet_interface()
{
    assert configure_inet_interface "-n '$1'"
    assert configure_inet_interface "$2 -eq 0 -o $2 -eq 1"

    dev=$1
    netmask=""
    state=""
    ret=0
    
    case "$2" in
    0) state=down ;;
    1) state=up   ;;
    esac

    if test "$VERBOSE" = "1"
    then
        $ECHO "Configuration \`$dev':"
        $ECHO "  address: ${ADDR:-<none>}"
        $ECHO "  netmask: ${NETMASK:-<none>}"
        $ECHO "  gateway: ${GATEWAY:-<none>}"
    fi
    
    [ -n "$NETMASK" ] && netmask="netmask $NETMASK"


    [ "$VERBOSE" = "1" ] && $ECHO "Setting IP-address..."

    $DRY_RUN /sbin/ifconfig $dev $ADDR $netmask $state >/dev/null 2>&1
    ret=$?
    if test $ret -eq 0
    then
        if test -n "$GATEWAY"
        then
            [ "$VERBOSE" = "1" ] && $ECHO "Setting gateway..."

            $DRY_RUN /sbin/route add gw $GATEWAY
            ret=$?
        fi
    fi
    
    return $ret
}


#-------------------------------------------------------------------------------


################################################################################
#                                                                              #
#                         S T A R T  O F  S C R I P T                          #
#                                                                              #
#                                                                              #
# OPERATION                                                                    #
#                                                                              #
#   - Process command line arguments.                                          #
#   - Call appropriate ACTION function.                                        #
#                                                                              #
################################################################################

QUIET=0                             # quiet flag (0 or 1)
VERBOSE=0                           # verbose flag (0 or 1)
DRY_RUN=""                          # default is to actually execute commands

ACTION=""                           # action to perform...
INTERFACES=`find_configs`           # ..on this interface(s)
                                    # default: all pre-defined interfaces


#
# Handle command line OPTIONS.
#
while test $# -gt 0
do
    case "$1" in
    -l|--list)
        list_interfaces
        ;;
    -n|--dry-run)
        DRY_RUN="$ECHO -e \n> "     # ensure we begin on a new line
        ;;
    -q|--quiet)
        QUIET=1
        exec >/dev/null             # mute stdout
        ;;
    -v|--verbose)
        VERBOSE=1
        ECHO_N=$ECHO                # don't inhibit newline;
                                    # this prevents clobbering of text
        ;;
    -h|--help)
        usage
        ;;
    -V|--version)
        version
        ;;
    -*)
        fatal "unknown option: $1"
        ;;
    *)                              # first non-OPTIONS argument
        break
        ;;
    esac
    shift
done

#
# Handle influential environment variables.
#
if test "$RC_VERBOSE" = "0" -o "$RC_VERBOSE" = "1"
then
    VERBOSE=$RC_VERBOSE
fi


#
# Determine what to do for who (ACTION and INTERFACES).
#

# try to derive ACTION and INTERFACES from script name: 
ACTION=`get_action $RC_NAME`
INTERFACES=`get_facility $RC_NAME`
if test "$INTERFACES" = "network"
then
    INTERFACES=`list_defined_ifs`
fi


#
# Command line parameters MAY override the
# derived ACTION and INTERFACES, if they've
# been specified. Multiple interface
# arguments are allowed.
#
if test -n "$1"; then ACTION=$1; shift; fi
if test -n "$1"; then INTERFACES=$*; fi

if test -n "$ACTION"
then
    if test -n "$INTERFACES"
    then
        case "$ACTION" in
        start)   up     "$INTERFACES" ;;
        stop)    down   "$INTERFACES" ;;
        restart) reinit "$INTERFACES" ;;
        status)  status "$INTERFACES" ;;
        *)
            fatal "unknown action: $ACTION"
            ;;
        esac
    else
        warning "no interface"
    fi
else
    fatal "missing action"
fi
