#!/bin/bash

# Copyright (c) 2017 SUSE Linux GmbH
#
# This file is part of cloud-netconfig.
#
# cloud-netconfig 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 3 of the License, or
# (at your option) any later version.
#
# cloud-netconfig 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 cloud-netconfig.  If not, see <http://www.gnu.org/licenses/

PROGNAME="cloud-netconfig"
# 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"
# for get_ipv4_addresses_from_metadata()
. "$r/etc/sysconfig/network/scripts/functions.cloud-netconfig"

STATEDIR=/var/run/netconfig

# -------------------------------------------------------------------
# return all IPv4 addresses currently configured on given interface
# 
get_ipv4_addresses_from_interface()
{
    local idx type addr iface="$1"
    test -z "$iface" && return 1
    ip -o -4 addr show dev $iface | while read -r idx iface_r type addr rest ; do
        echo -n "${addr} "
    done
}

# -------------------------------------------------------------------
# return all IPv6 addresses currently configured on given interface
# 
get_ipv6_addresses_from_interface()
{
    local idx iface_r scope_lit scope type addr rest iface="$1"
    test -z "$iface" && return 1
    ip -o -6 addr show dev $iface | \
    while read -r idx iface_r type addr scope_lit scope rest ; do
        # skip anything that is not scope global
        if [[ "$scope" == "global" ]]; then
            echo -n "${addr} "
        fi
    done
}

# -------------------------------------------------------------------
# compare arrays and return the ones that are in the first but not
# the second
# 
get_extra_elements()
{
    local actual=(${!1}) target=(${!2})
    local elem_a elem_t
    for elem_a in ${actual[@]} ; do
        found=0
        for elem_t in ${target[@]} ; do
            if [ "$elem_a" == "$elem_t" ]; then
                found=1
                break
            fi
        done
        test $found = 0 && echo -n "$elem_a "
    done
}

# -------------------------------------------------------------------
# look up a free priority with a given prefix from the rule table
# 
get_free_priority()
{
    local prefix="$1"
    test -z "$prefix" && return 1

    local IPV
    [[ -n "$2" ]] && IPV="-${2}"

    local prio=$((prefix*100))
    local taken
    while [[ $prio -lt $((prefix*100+100)) ]]; do
        taken=0
        while read -r no rest ; do
            if [[ "${prio}" == "${no%%:}" ]]; then
                taken=1
                break
            fi
        done < <(ip $IPV rule show)
        if [[ $taken == 0 ]]; then
            echo -n "${prio}"
            return
        fi
        prio=$((prio+1))
    done
    warn "No free priority for prefix $prefix"
    echo -n "29999"
}

# -------------------------------------------------------------------
# configure interface with secondary IPv4 addresses configured in the
# cloud framework and set up routing policies
# 
configure_interface_ipv4()
{
    local cfg="$1"
    test -z "$cfg" -o ! -f "$cfg" && return 1
    local INTERFACE IPADDR NETWORK NETMASK BROADCAST GATEWAYS
    get_variable "INTERFACE" "$cfg"
    get_variable "IPADDR" "$cfg"
    get_variable "NETWORK" "$cfg"
    get_variable "NETMASK" "$cfg"
    get_variable "BROADCAST" "$cfg"
    get_variable "GATEWAYS" "$cfg"
    local HWADDR="$(cat /sys/class/net/${INTERFACE}/address)"
    local RTABLE="10${INTERFACE: -1}"

    # get active and configured addresses
    local laddrs=($(get_ipv4_addresses_from_interface $INTERFACE))
    local raddrs=($(get_ipv4_addresses_from_metadata $HWADDR))

    # get differences
    local addr_to_remove=($(get_extra_elements laddrs[@] raddrs[@]))
    local addr_to_add=($(get_extra_elements raddrs[@] laddrs[@]))
    
    debug "active IPv4 addresses on $INTERFACE: ${laddrs[@]}"
    debug "configured IPv4 addresses on $INTERFACE: ${raddrs[@]}"
    debug "addresses to remove on $INTERFACE: ${addr_to_remove[@]}"
    debug "addresses to add on $INTERFACE: ${addr_to_add[@]}"

    local addr priority
    for addr in ${addr_to_remove[@]} ; do
        # as a safety measure, check against the address received via DHCP and
        # refuse to remove it even if it is not in the meta data
        if [[ "${addr%/*}" == "${IPADDR%/*}" ]]; then
            debug "not removing DHCP IP address from interface"
            continue
        fi

        # remove old IP address
        log "removing address $addr from interface $INTERFACE"
        ip -4 addr del $addr dev $INTERFACE

        # drop routing policy rule
        ip -4 rule del from ${addr%/*}
    done
    for addr in ${addr_to_add[@]} ; do
        # add new IP address
        log "adding address $addr to interface $INTERFACE"
        ip -4 addr add $addr broadcast $BROADCAST dev $INTERFACE
    done

    # To make a working default route for secondary interfaces, we
    # need a separate route table so we can send packets there
    # using routing policy. Check whether the table is there and
    # create it if not.
    if [ -z "$(ip -4 route show default dev $INTERFACE table $RTABLE)" ]; then
        local GW GWS=()
        # we simply take the first gateway in case there is more than one
        eval GWS=\(${GATEWAYS}\)
        if [[ -n "${GW[0]}" ]]; then
            GW="${GW[0]}"
        else
            # no default route, guess one
            # FIXME: this is not ideal; make it at least configurable
            GW="${NETWORK%.*}.1"
        fi
        debug "adding default route for $INTERFACE table $RTABLE"
        ip -4 route add default via $GW dev "$INTERFACE" table "$RTABLE"
    fi

    # update routing policies so connections from addresses on
    # secondary interfaces are routed via those
    local found prio from ip rest
    for addr in ${raddrs[@]} ; do
        found=0
        while read -r prio from ip rest ; do
            if [[ "${addr%/*}" == "$ip" ]]; then
                found=1
                break
            fi
        done < <(ip -4 rule show)
        if [[ $found == 0 ]]; then
            priority=$(get_free_priority $RTABLE 4)
            ip -4 rule add from ${addr%/*} priority $priority table $RTABLE
        fi
    done
}

# -------------------------------------------------------------------
# set up IPv6 routing policies
# 
configure_interface_ipv6()
{
    local cfg="$1"
    test -z "$cfg" -o ! -f "$cfg" && return 1
    get_variable "INTERFACE" "$cfg"

    # NOTE: unlike with IPv4, we set up routing policies even for the primary
    # interface. Default routes for IPv6 will likely have the same metric, so
    # unlike with the IPv4 routes, where we use higher metrics for the non
    # primary interfaces, we may run into the situation where the kernel picks
    # the default route of a non primary interface as the main default. Using
    # routing policies prevents that, but it means there will be a routing
    # policy even if you do not have a secondary interface at all (not a
    # problem, just not as nice as it could be).

    local RTABLE="10${INTERFACE: -1}"

    # if necessary, create route table with default route for interface
    if [ -z "$(ip -6 route show default dev $INTERFACE table $RTABLE)" ]; then
        # it is possible that we received the DHCP response before the
        # the router advertisement; in that case, we wait up to 10 secs
        local route via GW rest counter=0
        while [[ $counter -lt 10 ]]; do
            counter=$((counter+1))
            read -r route via GW rest < <(ip -6 route show default dev $INTERFACE)
            if [[ -n "$GW" ]]; then
                break
            else
                sleep 1
            fi
        done
        if [[ -z "$GW" ]]; then
            warn "no IPv6 default route available for $INTERFACE"
            return
        fi
        ip -6 route add default via $GW dev $INTERFACE table $RTABLE
    fi

    local addr laddrs=($(get_ipv6_addresses_from_interface $INTERFACE))
    local found prio from ip rest
    for addr in ${laddrs[@]} ; do
        found=0
        while read -r prio from ip rest ; do
            if [[ "${addr%/*}" == "$ip" ]]; then
                found=1
                break
            fi
        done < <(ip -6 rule show)
        if [[ $found == 0 ]]; then
            priority=$(get_free_priority $RTABLE 6)
            ip -6 rule add from ${addr%/*} priority $priority table $RTABLE
        fi
    done
}

# -------------------------------------------------------------------
# check if interface is configured to be managed by cloud-netconfig
# and whether it is DHCP configured; if yes, apply settings from
# the cloud framework
# 
manage_interfaceconfig()
{
    local cfg="$1"
    test -z "$cfg" -o ! -d "$cfg" && return 1
    local ifcfg="/etc/sysconfig/network/ifcfg-${cfg##*/}"
    local CLOUD_NETCONFIG_MANAGE
    if [[ -f "${ifcfg}" ]]; then
        get_variable "CLOUD_NETCONFIG_MANAGE" "${ifcfg}"
    fi
    if [[ "$CLOUD_NETCONFIG_MANAGE" != "yes" ]]; then
        # do not touch interface
        debug "Not managing interface ${cfg##*/}"
        return
    fi
    for cfg in ${1}/* ; do
        test -f $cfg || continue
        get_variable "SERVICE" "$cfg"
        case "$SERVICE" in
        "wicked-dhcp-ipv4"|"dhcpcd")
            metadata_available && configure_interface_ipv4 "$cfg"
         ;;
        "wicked-dhcp-ipv6"|"dhcp6c")
            metadata_available && configure_interface_ipv6 "$cfg"
        ;;
        esac
    done
}

for IFDIR in $STATEDIR/* ; do 
    test -d $IFDIR -a -d "/sys/class/net/${IFDIR##*/}" || continue
    manage_interfaceconfig $IFDIR
done
