#!/bin/bash
#
# Copyright (c) 2008 SUSE LINUX Products GmbH, Nuernberg, Germany.
# 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 (at your option) 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, see <http://www.gnu.org/licenses/>.
#
# Author: Michael Calmer <mc@suse.de>
#         Marius Tomaschewski <mt@suse.de>
#

#
# Exit codes
#
# 0  success
# 1  error
# 20 a module was not able to change the configuration
#    because it was changed since the last run
#

unset POSIXLY_CORRECT ; set +o posix # we are using non-posix bash features

# The environment variable ROOT indicates the root of the system to be
# managed by SuSEconfig when that root is not '/'
r=$ROOT

. $r/etc/sysconfig/network/scripts/functions.netconfig

PROGNAME=${0##*/}
STATEDIR=$r/run/netconfig/

MODULEDIR=$r/etc/netconfig.d/

. $r/etc/sysconfig/network/config

INPUTFORMAT=dhcpcd

function prepend() {
    test "x$1" != "x" -a -f "$1" || return 0
    netconfig_kv_filter "$1" | grep -Evs '^(SERVICE|INTERFACE)='
}

function modify() {
    if [ "x$SERVICE" = "x" ]; then
        warn "No service declaration available"
        return 1
    fi

    if test "x${LEASEFILE}" != x -a ! -r "${LEASEFILE}" ; then
        warn "Unable to read specified input file '${LEASEFILE}'"
        return 1
    fi

    local a b
    IFS=. read a b < /proc/uptime
    local CREATETIME=$a
    local _INTERFACE='' _SERVICE=''
    local -a KEYS VALS
    local _PREPEND=''

    # prepend previous NetworkManager policy settings
    if [ "x$SERVICE"        = "xNetworkManager" -a \
         "x$INTERFACE"      = "x" -a \
         "x$MODFILTER"     != "x" ] ;
    then
        _PREPEND="$STATEDIR/NetworkManager.netconfig"
    fi

    if [ "$INPUTFORMAT" == "dhcpcd" ]; then
        local key val err=0
        # add CREATETIME at index 0 !!
        KEYS=('CREATETIME'  'SERVICE')
        VALS=("$CREATETIME" "$SERVICE")
        while IFS== read -sr key val ; do
            test -n "$key" || {
                test -n "$val" || continue
                err=$val ; break
            }
            case $key in
                (CREATETIME) continue ;;
                (SERVICE|INTERFACE)
                    eval "_$key='$val'"
                    if test "x${!key}" != x -a "x${!key}" != "x$val" ; then
                        warn "Input value $key conflicts with command line argument"
                        return 1
                    fi
                ;;
            esac
            KEYS[${#KEYS[@]}]=$key
            VALS[${#VALS[@]}]=$val
        done < <(prepend "$_PREPEND" ; netconfig_kv_filter ${LEASEFILE:+"$LEASEFILE"})
        if test $err -ne 0 ; then
            warn "Invalid syntax in ${LEASEFILE:+${LEASEFILE}, }line $err"
            return 1
        fi
    else
        warn "Unsupported lease file / input format '$INPUTFORMAT'"
        return 1
    fi

    # NetworkManager policy merged settings
    if [ "x$SERVICE"        = "xNetworkManager" -a \
         "x$INTERFACE"      = "x" ] ;
    then
        # NetworkManager policy merged config
        CFGFILE="$STATEDIR/NetworkManager.netconfig"

        # first get the createtime. we want to keep it.
        get_variable CREATETIME "$CFGFILE"
        KEYS[0]='CREATETIME'
        VALS[0]="$CREATETIME"

        # remove it also in modify.
        # We want to create it with the same name again.
        rm -f "$CFGFILE"
    else
        # use interface name from config if provided
        if [ "x$INTERFACE" = "x" -a "x$_INTERFACE" != "x" ]; then
            INTERFACE="$_INTERFACE"
        fi

        if [ "x$INTERFACE" = "x" ]; then
            warn "No interface declaration available"
            return 1
        fi

        if [ ! -d "$STATEDIR/$INTERFACE" ]; then
            mkdir -p "$STATEDIR/$INTERFACE" || {
                warn "Can not create interface state dir"
                return 1
            }
        fi

        local CFGFILE=`find_cfgfile "$INTERFACE" "$SERVICE"`
        if [ "x$CFGFILE" != "x" ]; then
            # first get the createtime. we want to keep it.
            get_variable CREATETIME "$CFGFILE"
            KEYS[0]='CREATETIME'
            VALS[0]="$CREATETIME"

            # remove it also in modify. 
            # We want to create it with the same name again.
            rm -f "$CFGFILE"
        fi

        if [ "x$CFGFILE" = "x" ]; then
            local CFGNAME=`get_new_cfgname`
            CFGFILE="$STATEDIR/$INTERFACE/$CFGNAME"
        fi
   fi

   debug "write new STATE file $CFGFILE"
   for((i=0; i<${#KEYS[@]} && i<${#VALS[@]}; i++)) ; do
       printf "%s='%s'\n" "${KEYS[$i]}" "${VALS[$i]}"
   done > "$CFGFILE"

   return 0
}

function remove() {
    if [ "x$SERVICE" = "x" ]; then
        warn "No service declaration available"
        return 1
    fi

    # NetworkManager policy merged settings
    if [ "x$SERVICE"        = "xNetworkManager" -a \
         "x$INTERFACE"      = "x" ] ;
    then
        # NetworkManager policy merged config
        CFGFILE="$STATEDIR/NetworkManager.netconfig"

    else
        if [ "x$INTERFACE" = "x" ]; then
            warn "No interface declaration available"
            return 1
        fi

        CFGFILE=`find_cfgfile "$INTERFACE" "$SERVICE"`
    fi

    if [ "x$CFGFILE" != "x" ]; then
        debug "remove STATE file $CFGFILE"
        rm -f "$CFGFILE"
    fi

    return 0
}

#
# find_cfgfile <interface> <service>
#
function find_cfgfile() {

    test -z "$1" && return 1
    test -z "$2" && return 1

    local INTERFACE=$1
    local CUR_SERVICE=$2

    for CFG in `ls -X $STATEDIR/$INTERFACE/ 2>/dev/null`; do
        get_variable "SERVICE" $STATEDIR/$INTERFACE/$CFG

        if [ "$CUR_SERVICE" = "$SERVICE" ]; then
                CFGNAME=$CFG
                echo "$STATEDIR/$INTERFACE/$CFG"
                return;
        fi
    done
    return 0;
}

function get_new_cfgname() {
    local CFGNAME=""

    for CFG in `ls -X -r $STATEDIR/$INTERFACE/ 2>/dev/null`; do
        CNT="${CFG#netconfig}"
        ((CNT++))
        CFGNAME="netconfig$CNT"
        break
    done

    if [ "x$CFGNAME" = "x" ]; then
        CFGNAME="netconfig0"
    fi
    echo $CFGNAME;
}

function run_modules() {
    local mfilter="$1"
    local errors=()
    local msg=""
    local ret=0 err=0

    if [ -n "$FORCE_REPLACE" ]; then
        export FORCE_REPLACE=$FORCE_REPLACE
    fi
    if [ -n "$r" ]; then
        export ROOT=$r
    fi

    debug "Module order: $NETCONFIG_MODULES_ORDER"
    for plg in $NETCONFIG_MODULES_ORDER; do
        case $plg in
            -*)
                debug "${plg:1} module is disabled" ;
                continue
            ;;
        esac
        if [ ! -x "$MODULEDIR/$plg" ]; then
            debug "$plg module is not executable"
            continue
        fi
        case "x$mfilter" in
            "x"|"x${plg}"|"x${plg%%-*}")   ;;
            *) debug "$plg module skipped" ;
               continue                    ;;
        esac

        msg=`"$MODULEDIR/$plg" 2>&1` ; ret=$?
        if [ "$ret" != "0" -a "$err" = "0" ]; then
            err=$ret
        fi
        if [ "x$msg" != x ]; then
            errors[${#errors[@]}]="$msg"
        fi
    done

    if [ ${#errors[@]} -gt 0 ]; then
        for msg in "${errors[@]}" ; do
            echo "$msg"
        done
    fi
    return $err
}

function batch ()
{
    local _BATCHFILE="$BATCHFILE"
    local _SERVICE="$SERVICE"
    local _INTERFACE="$INTERFACE"
    local _LEASEFILE="$LEASEFILE"
    local _INPUTFORMAT="$INPUTFORMAT"
    local _MODFILTER="$MODFILTER"
    local _FORCE_REPLACE="$FORCE_REPLACE"
    local _VERBOSE="$VERBOSE"
    local _QUIET="$QUIET"
    local _IFS="$IFS"
    local ERR RET args

    if test "X$_BATCHFILE" != "X" ; then
        test -f "$_BATCHFILE" || return 1
    fi

    ERR=0
    while read -rs -a args ; do
        set -- "${args[@]}"
        ACTION=""
        BATCHFILE=""
        SERVICE="$_SERVICE"
        INTERFACE="$_INTERFACE"
        LEASEFILE="$_LEASEFILE"
        INPUTFORMAT="$_INPUTFORMAT"
        MODFILTER="$_MODFILTER"
        FORCE_REPLACE="$_FORCE_REPLACE"
        VERBOSE="$_VERBOSE"
        QUIET="$_QUIET"
        while test $# -gt 0 ; do
            VARIABLE=''
            REGEX=''
            case "$1" in
                -s|--service)       VARIABLE=SERVICE ;
                                    REGEX='^[[:alnum:]_-]+$' ;;
                -i|--interface)     VARIABLE=INTERFACE ;
                                    REGEX='^[^'"'"'`"\\/[:space:][:cntrl:]]+$' ;;
                -l|--lease-file)    VARIABLE=LEASEFILE;;
                -I|--input-file)    VARIABLE=LEASEFILE;;
                -F|--input-format)  VARIABLE=INPUTFORMAT;;
                -m|--module-only)   VARIABLE=MODFILTER;;
                -f|--force-replace) FORCE_REPLACE="true";;
                -B|--batch-file)    VARIABLE=BATCHFILE;;
                -v|--verbose)       VERBOSE="yes"; QUIET="no";;
                -q|--quiet)         QUIET="yes"; VERBOSE="no";;
                -h|--help)          ;;
                "") break           ;;
                --)
                    shift
                    test -n "$ACTION" -o $# -gt 1 && {
                        debug "batch: exactly one action can be given."
                        continue 2
                    }
                    ACTION="$1"
                    shift
                    break
                ;;
                *)
                    test -n "$ACTION" && {
                        debug "batch: exactly one action may be given."
                        continue 2
                    }
                    ACTION="$1"
                ;;
                -*) debug "batch: unknown option '$1'" ; continue 2 ;;
            esac
            if [ -n "$VARIABLE" ] ; then
                case $2 in
                (""|-*) debug "batch: option $1 needs an argument" ;;
                esac
                if [ -n "$REGEX" ] ; then
                    if ! [[ ${1} =~ ${REGEX} ]] ; then
                        debug "Invalid character in value for option $VARIABLE"
                        continue 2
                    fi
                fi
                eval $VARIABLE=\$2
                shift
            fi
            shift
        done
        test "X$ACTION" = "Xmodify" -a -z "$LEASEFILE" && {
            debug "batch: input file required"
            continue
        }
        export VERBOSE QUIET ACTION
        RET=0
        case "$ACTION" in
            modify)
                if test -z "$LEASEFILE" ; then
                    debug "batch: input file required in modify action"
                else
                    modify
                fi
            ;;
            remove) remove                                  ;;
            update)
                run_modules "$MODFILTER" ; RET=$?
                if [ "$RET" != "0" -a "$ERR" = "0" ]; then
                    ERR=$RET
                fi
            ;;
            *) debug "batch: unsupported action '$ACTION'"  ;;
        esac
        IFS="$_IFS"
    done < <(cat -- ${_BATCHFILE:+"$_BATCHFILE"})

    return $ERR
}

function usage () 
{
    typeset -a MODULES
    typeset -a MGROUPS
    local grp g plg

    for plg in $NETCONFIG_MODULES_ORDER; do
        case $plg in
          -*) continue ;;
        esac
        if [ ! -x "$MODULEDIR/$plg" ]; then
            continue
        fi
        MODULES=(${MODULES[@]} $plg)
        grp=${plg%%-*}
        for g in ${MGROUPS[@]} ; do
            test "x$grp" = "x$g" && continue 2
        done
        MGROUPS=(${MGROUPS[@]} $grp)
    done
    cat << EOT >&2
Usage:
 $PROGNAME <global options>
 $PROGNAME <action> <action options>

 actions:
  modify    Requires an interface and service specific settings via STDIN
            or as file using the --input-file or --lease-file option.
            Already existing settings for this interface and service will
            be replaced with the new one, otherwise netconfig creates a
            new state file. Finally, netconfig updates the managed files.
  remove    Removes the interface and service specific settings and
            updates the managed files.
  update    Updates the managed files with the current set of settings.
  batch     Executes a batch file with modify,remove,update action lines.
            Unlike in regular actions, modify and remove do not call update.

 modify options:
  < -s|--service <service name> >       service providing settings
  [ -i|--interface <interface name> ]   interface providing settings
  [ -F|--input-format <input format> ]  currently 'dhcpcd' supported only
  [ -I|--input-file <file name> ]       file name to read, stdin by default
  [ -l|--lease-file <file name> ]       alias for --input-file
  [ -m|--module-only <name | prefix> ]  module or module group updates only
  [ -f|--force-replace ]                generate files, even user modified
  [ -v|--verbose ]                      enable debug and be verbose

 remove options:
  < -s|--service <service name> >       service providing settings
  [ -i|--interface <interface name> ]   interface providing settings
  [ -m|--module-only <name | prefix> ]  module or module group updates only
  [ -f|--force-replace ]                generate files, even user modified
  [ -v|--verbose ]                      enable debug and be verbose

 update options:
  [ -m|--module-only <name | prefix> ]  module or module group updates only
  [ -f|--force-replace ]                generate files, even user modified
  [ -v|--verbose ]                      enable debug and be verbose

 batch options:
  [ -B|--batch-file <file name> ]       file name to read, stdin by default
  [ -v|--verbose ]                      enable debug and be verbose

 global options:
  <-h|--help>                           show this help text

Active modules: ${MODULES[@]}
Module groups : ${MGROUPS[@]}

EOT
  if [ -n "$1" ] ; then
     ERROR="  ERROR:   "
     echo -e "$*\n" | while read line; do
       echo "${ERROR}${line}" >&2
       ERROR="           "
     done
  fi
  exit 1
}

COMMANDLINE="$*"
ACTION=""
SERVICE=""
INTERFACE=""
VARIABLE=""
MODFILTER=""
FORCE_REPLACE=false
QUIET=no
VERBOSE=no
BATCHFILE=""
test "X$NETCONFIG_VERBOSE" = "Xyes"       && VERBOSE="yes"
test "X$NETCONFIG_FORCE_REPLACE" = "Xyes" && FORCE_REPLACE="true"
while true ; do
    # Interface is checked against sysfs before the data is used;
    # reject just the most the ugliest characters...
    REGEX=''
    VARIABLE=''
    case "$1" in
        -s|--service)       VARIABLE=SERVICE ;
                            REGEX='^[[:alnum:]_-]+$' ;;
        -i|--interface)     VARIABLE=INTERFACE ;
                            REGEX='^[^'"'"'`"\\/[:space:][:cntrl:]]+$' ;;
        -l|--lease-file)    VARIABLE=LEASEFILE;;
        -I|--input-file)    VARIABLE=LEASEFILE;;
        -F|--input-format)  VARIABLE=INPUTFORMAT;;
        -m|--module-only)   VARIABLE=MODFILTER;;
        -f|--force-replace) FORCE_REPLACE="true";;
        -B|--batch-file)    VARIABLE=BATCHFILE;;
        -v|--verbose)       VERBOSE="yes"; QUIET="no";;
        -q|--quiet)         QUIET="yes"; VERBOSE="no";;
        -h|--help)          usage;;
        "") break ;;
        --)
            shift
            test -n "$ACTION" -o $# -gt 1 \
            && usage "Exactly one action may be given.\n"\
                "Currently given actions: $ACTION $*"
            ACTION="$1"
            shift
            break
            ;;
        *)
            test -n "$ACTION" && usage "Exactly one action may be given.\n"\
                                       "Currently given actions: $ACTION $1"
            ACTION="$1"
        ;;
        -*) usage Unknown option $1;;
    esac
    if [ -n "$VARIABLE" ] ; then
        case $2 in
        (""|-*) usage "Option $1 needs an argument" ;;
        esac
        if [ -n "$REGEX" ] ; then
            if ! [[ ${1} =~ ${REGEX} ]] ; then
                usage "Invalid character in value for option $VARIABLE"
            fi
        fi
        eval $VARIABLE=\$2
        shift
    fi
    shift
done

test -z "$ACTION" && usage No action was given

test "X$VERBOSE" = "Xyes" && log "Executing '$COMMANDLINE' for pid $PPID"
export VERBOSE QUIET ACTION

#
# before we start, check for root
#
if test "$UID" != "0" -a "$USER" != root -a -z "$ROOT" ; then
    log "You must be root to start $0."
    exit 1
fi

# Check if we can write in /tmp
if ! touch $r/tmp &>/dev/null; then
    log "Filesystem read only: Cannot modify anything"
    exit 1
fi

# Set useful default umask
umask 0022

# Create state directory
if ! mkdir -p "$STATEDIR" ; then
    log "Unable to create netconfig state directory '$STATEDIR'"
    exit 1
fi

#
# try to get the lock
#
openLockWait "$PROGNAME" 5
if [ "$?" != 0 ]; then
    log "open lock for $PROGNAME failed. Abort."
    exit 1;
fi

RET=0
ERR=0

case "$ACTION" in
    modify)
           modify                   ; RET=$?
           if [ "$RET" != "0" -a "$ERR" = "0" ]; then ERR=$RET; fi
           run_modules "$MODFILTER" ; RET=$?
           if [ "$RET" != "0" -a "$ERR" = "0" ]; then ERR=$RET; fi
           ;;
    update)
           run_modules "$MODFILTER" ; RET=$?
           if [ "$RET" != "0" -a "$ERR" = "0" ]; then ERR=$RET; fi
           ;;
    remove)
           remove                   ; RET=$?
           if [ "$RET" != "0" -a "$ERR" = "0" ]; then ERR=$RET; fi
           run_modules "$MODFILTER" ; RET=$?
           if [ "$RET" != "0" -a "$ERR" = "0" ]; then ERR=$RET; fi
           ;;
    batch)
           batch                    ; RET=$?
           if [ "$RET" != "0" -a "$ERR" = "0" ]; then ERR=$RET; fi
           ;;
    *)
    usage "Invalid action given.\n"
    ;;
esac

unLock "$PROGNAME"

exit $ERR

# vim: set ts=8 sts=4 sw=4 ai et:
