#!/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
ADDRDIR=/var/run/netconfig/cloud

# -------------------------------------------------------------------
# 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
}

# -------------------------------------------------------------------
# check wether $2 is an element of array $1
#
is_element()
{
    local array=(${!1}) item=${2}
    local elem
    for elem in ${array[@]} ; do
        if [[ $elem == $item ]]; then
            return 0
        fi
    done
    return 1
}

# -------------------------------------------------------------------
# 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"
}

# -------------------------------------------------------------------
# get IPv4 addresses from log dir
#
get_cn_assigned_addrs()
{
    local iface="$1"
    test -z "$iface" && return 1

    if [[ -e ${ADDRDIR}/${iface} ]]; then
        echo $(cat ${ADDRDIR}/${iface})
    fi
}

# -------------------------------------------------------------------
# get link status for interface
#
get_link_status()
{
    local iface="$1"
    test -z "$iface" && return 1

    ip -o -br link show dev $iface | awk '{ print $2 }'
}

# -------------------------------------------------------------------
# add address to log
#
add_addr_to_log()
{
    local iface="$1"
    local addr="$2"

    test -z "$iface" -o -z "$addr" && return 1
    addr_log_file="${ADDRDIR}/${iface}"
    if [[ -e ${addr_log_file} ]]; then
        grep -q -x "${addr}" "${addr_log_file}" || echo "${addr}" >> "${addr_log_file}"
    else
        mkdir -p "${ADDRDIR}"
        echo "${addr}" > "${addr_log_file}"
    fi
}

# -------------------------------------------------------------------
# remove address from log
#
remove_addr_from_log()
{
    local iface="$1"
    local addr="$2"

    test -z "$iface" -o -z "$addr" && return 1
    addr_log_file="${ADDRDIR}/${iface}"
    if [[ -e ${addr_log_file} ]]; then
        addr_log_file_tmp=$(mktemp ${ADDRDIR}/${iface}.XXXXXXXX)
        if [ $? -ne 0 ]; then
            log "could not create temp file, not removing address from log"
        else
            grep -v -x "${addr}" "${addr_log_file}" >> "${addr_log_file_tmp}"
            mv "${addr_log_file_tmp}" "${addr_log_file}"
        fi
    fi
}

# -------------------------------------------------------------------
# copy routes from default table to given table and remove routes
# that do not exist in default table (if any)
#
update_routing_table()
{
    local rtable="$1"
    local iface="$2"

    test -z "$rtable" && return 1

    # copy routes from default table
    for ipv in "-4" "-6" ; do
        ip $ipv route show | grep -v "^default" | while read route ; do
            ip $ipv route replace $route table $rtable
        done

        # Do a second run with routes specific to the interface being configured.
        # This makes sure that in case there are competing routes (same destination
        # but different device), the one matching the right interface is selected.
        # This situation can happen in case there are multiple interfaces that are
        # connected to the same subnet.
        ip $ipv route show dev "$iface" | grep -v "^default" | while read route ; do
            ip $ipv route replace $route dev "$iface" table $rtable
        done

        # check if there are any leftover routes and delete them
        ip $ipv route show all table $rtable | grep -v "^default" | while read route ; do
            ip_out="$(ip $ipv route show $route)"
            test -z "$ip_out" && ip $ipv route del $route table $rtable
        done
    done
}

# -------------------------------------------------------------------
# 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"
    get_variable "PREFIXLEN" "$cfg"
    get_variable "ROUTES" "$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[@]))

    # get addresses cloud-netconfig configured
    local addr_cn=($(get_cn_assigned_addrs $INTERFACE))

    debug "active IPv4 addresses on $INTERFACE: ${laddrs[@]}"
    debug "configured IPv4 addresses for $INTERFACE: ${raddrs[@]}"
    debug "excess addresses on $INTERFACE: ${addr_to_remove[@]}"
    debug "addresses to configure 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

        if is_element addr_cn[@] ${addr} ; then
            # 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%/*}

            # remove from address log
            remove_addr_from_log $INTERFACE $addr
        else
            debug "not removing address ${addr} (was not added by cloud-netconfig)"
        fi
    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
        add_addr_to_log $INTERFACE $addr
    done

    # If we have a single NIC configuration, skip routing policies
    if [[ $SINGLE_NIC = yes ]]; then
       return
    fi

    # 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 "${GWS[0]}" ]]; then
            GW="${GWS[0]}"
        else
            debug "No gateway from DHCP4, guessing"
            # no default route, guess one
            if [[ $PREFIXLEN = 32 ]]; then
                # peer-to-peer setup but no gateway. At the time of writing
                # that is the setup in GCE. Subnet routes and gateway get
                # delivered by DHCP as addtional routes.
                for r in $ROUTES ; do
                    if [[ ${r##*,} = 0.0.0.0 ]]; then
                        GW="${r%%,*}"
                        break
                    fi
                done
            else
                # we assume the gateway is the first host of the network
                local netstart=${NETWORK##*.}
                local gw_host_part=$((netstart+1))
                GW="${NETWORK%.*}.${gw_host_part}"
            fi
        fi
        if [[ -n $GW ]]; then
            debug "adding default route via $GW for $INTERFACE table $RTABLE"
            ip -4 route add default via $GW dev "$INTERFACE" table "$RTABLE"
        else
            warn "No default route for $INTERFACE"
        fi
    fi

    # copy specific routes from the default routing table
    update_routing_table $RTABLE $INTERFACE

    # 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
    linkstatus=$(get_link_status ${cfg##*/})
    if [[ $linkstatus != UP ]]; then
        debug "interface ${cfg##*/} is down"
        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
}

if_dirs=()
for IFDIR in $STATEDIR/* ; do
    test -d $IFDIR -a -d "/sys/class/net/${IFDIR##*/}" || continue
    test ${IFDIR##*/} = "lo" && continue
    if_dirs+=($IFDIR)
done

# set single NIC flag if appropriate
if [[ ${#if_dirs[@]} -eq 1 ]]; then
    SINGLE_NIC=yes
else
    SINGLE_NIC=no
fi

for IFDIR in ${if_dirs[@]} ; do
    manage_interfaceconfig $IFDIR
done
