#!/usr/bin/env bash

# -------------------------------------------------------------------------- #
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems                #
#                                                                            #
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
# not use this file except in compliance with the License. You may obtain    #
# a copy of the License at                                                   #
#                                                                            #
# http://www.apache.org/licenses/LICENSE-2.0                                 #
#                                                                            #
# Unless required by applicable law or agreed to in writing, software        #
# distributed under the License is distributed on an "AS IS" BASIS,          #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
# See the License for the specific language governing permissions and        #
# limitations under the License.                                             #
#--------------------------------------------------------------------------- #

# shellcheck disable=SC2155

#
# network module interface
#

export required_context_type='local' # this is the default, the next option is 'online'

is_network_supported()
{
    false
}

initialize_network()
{
    # shellcheck disable=SC2154
    case "${os_id}" in
        freebsd)
            :
            ;;
        *)
            mkdir -p /etc/sysctl.d/
            rm -f /etc/sysctl.d/50-one-context.conf
            sysctl --system 2>/dev/null || sysctl -p
            ;;
    esac

    #
    # remove all stale configs from any of the previous runs
    #

    # TODO: improve this and support proper network unconfigure and cleanup
    # on action=reconfigure too - this becomes problematic when NETCFG_TYPE is
    # changed while VM is running (deleting configs will break ifdown etc.)
    #
    # shellcheck disable=SC2154
    [ "${action}" = 'configure' ] || return 0

    export onegate_proxy_route_missing="yes" # flag route not setup
    get_onegate_ip

    local _context_interfaces=$(get_context_interfaces)
    local _iface_mac=$(get_interface_mac)

    for _iface in $_context_interfaces; do
        local _method=$(get_iface_var "${_iface}" "METHOD")

        # Do *not* cleanup the interface if user wants to manage it "manually".
        # NOTE: ETHx_METHOD="skip" (set in VM context) implies ETHx_IP6_METHOD="skip".
        if [ "${_method}" = 'skip' ]; then
            continue
        fi

        local _mac=$(get_iface_var "${_iface}" "MAC")
        local _dev=$(get_dev "${_iface_mac}" "${_mac}")

        # network-scripts
        rm -f \
            "/etc/sysconfig/network-scripts/route-${_dev}" \
            "/etc/sysconfig/network-scripts/route6-${_dev}" \
            "/etc/sysconfig/network-scripts/ifcfg-${_dev}" \
            "/etc/sysconfig/network/ifroute-${_dev}" \
            "/etc/sysconfig/network/ifsysctl-${_dev}" \
            "/etc/sysconfig/network/ifcfg-${_dev}"

        # networkd
        rm -f \
            "/etc/systemd/network/${_dev}.network" \
            "/etc/systemd/network/${_dev}.link"

        # nm (on RH systems it was deleted with ifcfg-*)
        for _nm_con in /etc/NetworkManager/system-connections/* ; do
            if [ -e "${_nm_con}" ] && grep -q "^interface-name=${_dev}$" "${_nm_con}" ; then
                rm -f "${_nm_con}"
            fi
        done
    done

    # To avoid clashes when running legacy network-scripts and
    # NetworkManager/networkd, we disable old-style networking
    # on Red Hats and enable later back only if needed.
    if [ -x /etc/sysconfig/network-scripts/ifup ]; then
        touch /etc/sysconfig/network
        sed -i -e '/^NETWORKING=/d' /etc/sysconfig/network
        echo 'NETWORKING=no' >>/etc/sysconfig/network
    fi

    # FIXME: This needs to be smarter so skipped NICs are not
    #        purged when defined manually by the user.
    # interfaces
    if [ -e /etc/network/interfaces ] ; then
        cat <<EOT >/etc/network/interfaces
# Generated by one-context
auto lo
iface lo inet loopback

EOT

        case "${os_id}" in
            debian|ubuntu|devuan)
                echo "source /etc/network/interfaces.d/*.cfg" >> /etc/network/interfaces
                ;;
        esac
    fi

    # netplan
    rm -f /etc/netplan/50-one-context.yaml

    nm_disable
}

nm_disable() {
    if [ -d /etc/NetworkManager/conf.d/ ] &&
       ! [ -e /etc/NetworkManager/conf.d/50-unmanaged-devices.conf ];
    then
        cat - <<EOF >/etc/NetworkManager/conf.d/50-unmanaged-devices.conf
# Generated by one-context

# NOTE: NetworkManager was dynamically disabled by OpenNebula
# contextualization scripts because interfaces are managed by
# different network service!

[keyfile]
unmanaged-devices=*
EOF

        if command -v systemctl >/dev/null; then
            systemctl --no-block try-reload-or-restart NetworkManager.service 2>/dev/null
        else
            service NetworkManager reload 2>/dev/null
        fi
    fi
}

nm_enable() {
    if [ -e /etc/NetworkManager/conf.d/50-unmanaged-devices.conf ]; then
        rm -f /etc/NetworkManager/conf.d/50-unmanaged-devices.conf

        if command -v systemctl >/dev/null; then
            systemctl --no-block try-reload-or-restart NetworkManager.service 2>/dev/null
        else
            service NetworkManager reload 2>/dev/null
        fi
    fi
}

configure_network()
{
    echo "ERROR [!]: No 'configure_network' implementation for the network type: ${NETCFG_TYPE}" >&2
    exit 1
}

stop_network()
{
    echo "ERROR [!]: No 'stop_network' implementation for the network type: ${NETCFG_TYPE}" >&2
    exit 1
}

start_network()
{
    echo "ERROR [!]: No 'start_network' implementation for the network type: ${NETCFG_TYPE}" >&2
    exit 1
}

reload_network()
{
    echo "ERROR [!]: No 'reload_network' implementation for the network type: ${NETCFG_TYPE}" >&2
    exit 1
}

#
# generic shared functions
#

# arg: <true|yes|false|no>
is_true()
(
    _value=$(echo "$1" | \
        sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | \
        tr '[:upper:]' '[:lower:]')
    case "$_value" in
        1|true|yes|y)
            return 0
            ;;
    esac

    return 1
)

# return OS ID
detect_os()
(
    if [ -f /etc/os-release ] ; then
        ID=
        ID_LIKE=
        LINUX_ID=
        # shellcheck disable=SC1091
        . /etc/os-release

        case "$ID" in
            # supported distros
            alpine|altlinux|debian|devuan|ubuntu|fedora|centos|rhel|almalinux|ol|rocky|opensuse*|sles|sled|amzn|freebsd)
                LINUX_ID=$ID
                ;;
            *)
                LINUX_ID=$ID_LIKE
                ;;
        esac

        echo "$LINUX_ID" | tr '[:upper:]' '[:lower:]'

    # check for legacy RHEL/CentOS 6
    elif [ -f /etc/centos-release ]; then
        echo 'centos'
    elif [ -f /etc/redhat-release ]; then
        echo 'rhel'

    # fallback to uname (returns Linux, FreeBSD, ...)
    else
        uname | tr '[:upper:]' '[:lower:]'
    fi
)

# return OS VERSION
detect_os_version()
(
    if [ -f /etc/os-release ] ; then
        VERSION_ID=
        # shellcheck disable=SC1091
        . /etc/os-release

        echo "$VERSION_ID"
    fi
)

# arg: <iface>
disable_ipv6()
(
    # shellcheck disable=SC2154
    case "${os_id}" in
        freebsd)
            # TODO: these are the relevant options in /etc/rc.conf:
            #   ip6addrctl_enable="NO"
            #   ip6addrctl_policy="ipv4_prefer"
            #   ipv6_activate_all_interfaces="NO"
            #   ipv6_network_interfaces="none"
            echo "ERROR [!]: Disabling of IPv6 on '${os_id}' is not supported" >&2
            ;;
        *)
            # VH-TODO: do we suport runtime enable?
            for S in \
                "net.ipv6.conf.${1}.disable_ipv6=1" \
                "net.ipv6.conf.${1}.autoconf=0" \
                "net.ipv6.conf.${1}.accept_ra=0";
            do
                # don't duplicate entries on recontextualization
                if ! grep -Fxq "${S}" /etc/sysctl.d/50-one-context.conf 2>/dev/null; then
                    echo "${S}" >> /etc/sysctl.d/50-one-context.conf
                fi

                sysctl -w "${S}" >/dev/null
            done
            ;;
    esac
)

# arg: <iface>
disable_ipv6_privacy()
(
    case "${os_id}" in
        freebsd)
            echo "ERROR [!]: Disabling of IPv6 privacy on '${os_id}' is not supported" >&2
            ;;
        *)
            # shellcheck disable=SC2066
            for S in \
                "net.ipv6.conf.${1}.use_tempaddr=0";
            do
                # don't duplicate entries on recontextualization
                if ! grep -Fxq "${S}" /etc/sysctl.d/50-one-context.conf 2>/dev/null; then
                    echo "${S}" >> /etc/sysctl.d/50-one-context.conf
                fi

                sysctl -w "${S}" >/dev/null
            done
            ;;
    esac
)

skip_interface()
{
    [ -z "${dev}" ] && return 0

    _skip4=
    case "${method}" in
        ''|static)
            if [ -z "${ip}" ] ; then
                _skip4=yes
            else
                _skip4=no
            fi
            ;;
        skip)
            _skip4=yes
            ;;
        dhcp)
            _skip4=no
            ;;
        *)
            echo "ERROR [!]: Unknown IPv4 method: ${method}, skipping" >&2
            _skip4=yes
            ;;
    esac

    _skip6=
    case "${ip6_method}" in
        ''|static)
            if [ -z "${ip6}" ] ; then
                _skip6=yes
            else
                _skip6=no
            fi
            ;;
        skip)
            _skip6=yes
            ;;
        disable)
            disable_ipv6 "${dev}"
            _skip6=yes
            ;;
        auto|dhcp)
            _skip6=no
            ;;
        *)
            echo "ERROR [!]: Unknown IPv6 method: ${ip6_method}" >&2
            _skip6=yes
            ;;
    esac

    if is_true "${_skip4}" && is_true "${_skip6}" ; then
        return 0
    fi

    return 1
}

# args: <iface> <name>
get_iface_var()
(
    _iface=$(echo "$1" | tr '[:lower:]' '[:upper:]')
    _var_name="${_iface}_${2}"
    eval "echo \"\${${_var_name}}\""
)

# Gets IP address from a given MAC
mac2ip()
(
    _mac="$1"

    _ip_a=$(echo "$_mac" | cut -d: -f 3)
    _ip_b=$(echo "$_mac" | cut -d: -f 4)
    _ip_c=$(echo "$_mac" | cut -d: -f 5)
    _ip_d=$(echo "$_mac" | cut -d: -f 6)

    echo "0x${_ip_a}.0x${_ip_b}.0x${_ip_c}.0x${_ip_d}"
)

mask2cidr()
(
    _mask="$1"
    _nbits=0
    IFS=.
    for _dec in $_mask ; do
        case "$_dec" in
            255) _nbits=$((_nbits + 8)) ;;
            254) _nbits=$((_nbits + 7)) ; break ;;
            252) _nbits=$((_nbits + 6)) ; break ;;
            248) _nbits=$((_nbits + 5)) ; break ;;
            240) _nbits=$((_nbits + 4)) ; break ;;
            224) _nbits=$((_nbits + 3)) ; break ;;
            192) _nbits=$((_nbits + 2)) ; break ;;
            128) _nbits=$((_nbits + 1)) ; break ;;
            0) break ;;
            *) echo "Error: $_dec is not recognised"; exit 1 ;;
        esac
    done
    echo "$_nbits"
)

# Gets the network part of an IP
# arg: <iface>
get_network()
(
    _network=$(get_iface_var "$1" "NETWORK")

    if [ -z "$_network" ]; then
        _ip=$(get_ip "$1")
        _mask=$(get_mask "$1")
        _network=$(awk -v ip="$_ip" -v mask="$_mask" 'END {
            split(ip, ip_b, "."); split(mask, mask_b, ".");
            for (i=1; i<=4; ++i) x = x "." and(ip_b[i], mask_b[i]);
            sub(/^./, "", x); print x; }' </dev/null)
    fi

    echo "$_network"
)

# Gets the network mask
# arg: <iface>
get_mask()
(
    _mask=$(get_iface_var "$1" "MASK")
    echo "${_mask:-255.255.255.0}"
)

# Gets device MTU
# arg: <iface>
get_mtu()
(
    # VH_TODO: drop default 1500, nekde se spoleha na tento default!
    _mtu=$(get_iface_var "$1" "MTU")
    echo "${_mtu:-1500}"
)

# Gets the network gateway
# arg: <iface>
get_gateway()
(
    get_iface_var "$1" "GATEWAY"
)

# arg: <iface>
get_ip()
(
    get_iface_var "$1" "IP"
)

# arg: <iface>
get_dns()
(
    get_iface_var "$1" "DNS"
)

# arg: <iface>
get_search_domain()
(
    get_iface_var "$1" "SEARCH_DOMAIN"
)

# arg: <iface>
get_interface_alias()
(
    # sed on freebsd does not recognize '+' - replacing with asterisk
    env | sed -n "s/^\(${1}_ALIAS[0-9][0-9]*\)_MAC=.*/\1/p" | sort
)

get_context_interfaces()
(
    # sed on freebsd does not recognize '+' - replacing with asterisk
    env | sed -n 's/^\(ETH[0-9][0-9]*\)_MAC=.*/\1/p' | sort
)

get_pci_interfaces()
(
    # sed on freebsd does not recognize '+' - replacing with asterisk
    env | sed -n 's/^\(PCI[0-9][0-9]*\)_MAC=.*/\1/p' | sort
)

get_interface_mac()
(
    ip link show | awk '/^[0-9]+: [A-Za-z0-9@]+:/ { device=$2; gsub(/:/, "",device); split(device,dev,"@")} /link\/ether/ { print dev[1]  " " $2 }'
)

get_dev()
(
    _list="$1"
    _mac="$2"

    echo "$_list" | grep "$_mac" | cut -d' ' -f1 | tail -n1
)

# arg: <interface/alias>
setup_ipadr_vars()
{
    export ip=$(get_ip "$1")
    export network=$(get_network "$1")
    export mask=$(get_mask "$1")
    export cidr=$(mask2cidr "$mask")
}

# arg: <interface/alias>
setup_ip6adr_vars()
{
    export ip6=$(get_iface_var "$1" "IP6")
    export ip6_prefix_length=$(get_iface_var "$1" "IP6_PREFIX_LENGTH")
    export ip6_ula=$(get_iface_var "$1" "IP6_ULA")

    [ -z "$ip6" ] && ip6=$(get_iface_var "$1" "IPV6")
    [ -z "$ip6_prefix_length" ] && ip6_prefix_length=64
}

# arg: <interface>
setup_iface_vars()
{
    _iface_mac=$(get_interface_mac)

    export mac=$(get_iface_var "$1" "MAC")
    export dev=$(get_dev "$_iface_mac" "$mac")
    export mtu=$(get_iface_var "$1" "MTU")
    export gateway=$(get_gateway "$1")
    export metric=$(get_iface_var "$1" "METRIC")
    export dns=$(get_dns "$1")
    export search_domains=$(get_search_domain "$1")
    export method=$(get_iface_var "$1" "METHOD")
    export ip6_gateway=$(get_iface_var "$1" "IP6_GATEWAY")
    export ip6_metric=$(get_iface_var "$1" "IP6_METRIC")
    export ip6_method=$(get_iface_var "$1" "IP6_METHOD")
    export static_routes=$(get_iface_var "$1" "ROUTES")

    # backward compatibility
    [ -z "$ip6_gateway" ] && ip6_gateway=$(get_iface_var "$1" "GATEWAY6")

    # defaults
    [ -z "$ip6_metric" ] && ip6_metric="${metric}"
    [ -z "$method"  ]    && method='static'
    [ -z "$ip6_method" ] && ip6_method="${method}"

    setup_ipadr_vars "$1"
    setup_ip6adr_vars "$1"
}

# arg: <alias>
setup_alias_vars()
{
    export external=$(get_iface_var "$1" "EXTERNAL")
    export detach=$(get_iface_var "$1" "DETACH")
}

get_nameservers()
(
    # sed on freebsd does not recognize '+' - replacing with asterisk
    _dns_variables=$(env | sed -n 's/^\(ETH[0-9][0-9]*_DNS\)=.*/\1/p' | sort)

    for _dns in DNS ${_dns_variables} ; do
        _value=$(eval "echo \"\${$_dns}\"")
        if [ -n "$_value" ] ; then
            echo "$_value"
        fi
    done
)

get_searchdomains()
(
    # sed on freebsd does not recognize '+' - replacing with asterisk
    _search_domains=$(env | sed -n 's/^\(ETH[0-9][0-9]*_SEARCH_DOMAIN\)=.*/\1/p' | sort)

    for _search in SEARCH_DOMAIN ${_search_domains} ; do
        _value=$(eval "echo \"\${$_search}\"")
        if [ -n "$_value" ] ; then
            echo "$_value"
        fi
    done
)

gen_resolvconf()
{
    export all_nameservers=$(get_nameservers)
    export all_search_domains=$(get_searchdomains)

    [ -z "$all_nameservers" ] && return 0

    if [ -L /etc/resolv.conf ]; then
        unlink /etc/resolv.conf
    else
        cat /dev/null > /etc/resolv.conf
    fi

    for _nameserver in $all_nameservers ; do
        echo "nameserver ${_nameserver}" >> /etc/resolv.conf
    done

    if [ -f /etc/sysconfig/network/config ]; then
        sed -i "/^NETCONFIG_DNS_STATIC_SERVERS=/ s/=.*$/=\"${all_nameservers}\"/" /etc/sysconfig/network/config
    fi

    [ -z "$all_search_domains" ] && return 0

    echo "search ${all_search_domains}" >> /etc/resolv.conf

    if [ -f /etc/sysconfig/network/config ]; then
        sed -i "/^NETCONFIG_DNS_STATIC_SEARCHLIST=/ s/=.*$/=\"${all_search_domains}\"/" /etc/sysconfig/network/config
    fi
}


# 0 if https://en.wikipedia.org/wiki/Link-local_address
is_link_local() {
    [[ $1 == "169.254."* ]]
}

get_onegate_ip() {
	if [[ -n $ONEGATE_ENDPOINT ]]; then
		# Regular expression to match an IPv4 address
		ipv4_regex="([0-9]{1,3}\.){3}[0-9]{1,3}"

		export onegate_host=$(echo "$ONEGATE_ENDPOINT" | grep -oE "$ipv4_regex")
	fi
}

missing_onegate_proxy_route() {
    is_link_local "$onegate_host" && [[ $onegate_proxy_route_missing == "yes" ]]
}
