#!/bin/bash

# Idempotent script to create an ovs bridge and add a physical NIC
# onto it as an OVS port. Any IP address on the physical NIC will
# be automatically moved onto the OVS bridge.
#
# Uses the OS default networking script format to write out persistent
# config files to disk. These config files persist across reboots
# an are only updated if metadata changes are detected (these are passed
# in as script parameters). If a config change is made the network interface
# and OVS bridge are restarted using ifdown/ifup scripts.
#
# If public_interface_route is set then the current default route is
# specialised to a 169.254.169.254/32 only route (unless there is already a
# 169.254.169.254 route - such as a neutron network with host routes can
# create) and a default route via public_interface_route is added on the public
# interface.

set -eu

PATH=/usr/local/bin:$PATH

SCRIPT_NAME=$(basename $0)
SCRIPT_HOME=$(dirname $0)

function show_options () {
    echo "Usage: $SCRIPT_NAME [options] <br> <interface> [<ip> [<route>]]"
    echo
    echo "Create a bridge br with an external port interface."
    echo
    echo "This will ensure that br exists and that interface is a port on it."
    echo "If IP is not empty, it will be set as the IP address for the bridge."
    echo "Otherwise the bridge will be configured for DHCP. If route is supplied"
    echo "it will be used as the default route."
    echo "Public-tag and public-tag-ip must both be empty, or both set."
    echo
    echo "Options:"
    echo "    -h|--help       -- this help."
    echo "    --bridge-route  -- Add a route to the bridge, e.g. to IPMI network."
    echo "                       Accepts one parameter, the prefix and via with a"
    echo "                       space between them."
    echo "    --public-tag    -- Make int_public an access port with this tag."
    echo "    --public-tag-ip -- Give int_public this IP address."
    echo
    exit $1
}

BRIDGE_ROUTE=
PUBLIC_TAG=
PUBLIC_TAG_IP=

TEMP=$(getopt -o h -l bridge-route:,help,public-tag:,public-tag-ip: -n $SCRIPT_NAME -- "$@")
if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi

# Note the quotes around `$TEMP': they are essential!
eval set -- "$TEMP"

while true ; do
    case "$1" in
        -h|--help) show_options 0;;
        --bridge-route) BRIDGE_ROUTE=$2; shift 2;;
        --public-tag) PUBLIC_TAG=$2; shift 2;;
        --public-tag-ip) PUBLIC_TAG_IP=$2; shift 2;;
        --) shift ; break ;;
        *) echo "Error: unsupported option $1." ; exit 1 ;;
    esac
done

EXTERNAL_BRIDGE="$1"
PHYSICAL_INTERFACE="$2"
PHYSICAL_INTERFACE_IP_NETMASK="${3:-}" #optional, by default uses DHCP
PUBLIC_INTERFACE_ROUTE="${4:-}" #optional
EXTRA=${5:-""}

if [ -z "$PHYSICAL_INTERFACE" -o -n "$EXTRA" ]; then
    show_options 1
fi

if [ \( -n "$PUBLIC_TAG" -a -z "$PUBLIC_TAG_IP" \) -o \
     \( -z "$PUBLIC_TAG" -a -n "$PUBLIC_TAG_IP" \) ]; then
    # For now, we don't support DHCP on vlans - the next gen stuff will be well
    # layered and do that.
    echo "Only one of --public-tag and --public-tag-ip supplied." >&2
    show_options 1
fi

if [ -n "$BRIDGE_ROUTE" ]; then
    read -s BRIDGE_ROUTE_PREFIX BRIDGE_ROUTE_VIA <<< $BRIDGE_ROUTE
    if [ -z "$BRIDGE_ROUTE_PREFIX" -o -z "$BRIDGE_ROUTE_VIA" ]; then
        echo "Invalid route '$BRIDGE_ROUTE'" >&2
        show_options 1
    fi
else
    BRIDGE_ROUTE_PREFIX=
    BRIDGE_ROUTE_VIA=
fi

set -x

# network scripts function used on Fedora/RHEL/Centos, etc.
function configure_bridge_interface_dhcp_netscripts() {

    local bridge=$1
    local interface=$2
    local public_ip_addr=${3:-''}
    local public_ip_netmask=${4:-''}
    local interface_mac=${5}
    local public_tag=${6}
    local public_tag_ip=${7}
    local public_tag_ip_netmask=${8}
    local bridge_route_prefix=${9}
    local bridge_route_via=${10}
    local tmp_bridge_config=$(mktemp)
    local tmp_bridge_route=$(mktemp)
    local tmp_interface_config=$(mktemp)
    local tmp_int_public_config=$(mktemp)
    local bridge_config="/etc/sysconfig/network-scripts/ifcfg-$bridge"
    local bridge_route="/etc/sysconfig/network-scripts/route-$bridge"
    local interface_config="/etc/sysconfig/network-scripts/ifcfg-$interface"
    local int_public_config="/etc/sysconfig/network-scripts/ifcfg-int_public"

    #interface config
    cat > $tmp_interface_config <<EOF_CAT
DEVICE=$interface
ONBOOT=yes
DEVICETYPE=ovs
TYPE=OVSPort
OVS_BRIDGE=$bridge
BOOTPROTO=none
HOTPLUG=no
EOF_CAT

#bridge config
    if [ -z "$public_ip_addr" ]; then
        # DHCP for the bridge itself.
        cat > $tmp_bridge_config <<EOF_CAT
DEVICE=$bridge
ONBOOT=yes
DEVICETYPE=ovs
TYPE=OVSBridge
OVSBOOTPROTO="dhcp"
OVSDHCPINTERFACES=$interface
OVS_EXTRA="set bridge $bridge other-config:hwaddr=$interface_mac"
HOTPLUG=no
EOF_CAT
    else
        cat > $tmp_bridge_config <<EOF_CAT
DEVICE=$bridge
ONBOOT=yes
DEVICETYPE=ovs
TYPE=OVSBridge
OVS_EXTRA="set bridge $bridge other-config:hwaddr=$interface_mac"
BOOTPROTO=static
IPADDR=$public_ip_addr
NETMASK=$public_ip_netmask
HOTPLUG=no
EOF_CAT
    fi
    if [ -n "$bridge_route_prefix" ]; then
        echo $bridge_route_prefix via $bridge_route_via dev $bridge > $tmp_bridge_route
    fi
    if [ -n "$public_tag" ]; then
        # Setup the access port
        cat > $tmp_int_public_config <<EOF_CAT
DEVICE=int_public
ONBOOT=yes
DEVICETYPE=ovs
TYPE=OVSIntPort
BOOTPROTO=static
IPADDR=$public_tag_ip
NETMASK=$public_tag_ip_netmask
OVS_BRIDGE=$bridge
OVS_OPTIONS="tag=$public_tag"
HOTPLUG=no
EOF_CAT
    fi

    if ! diff $tmp_interface_config $interface_config &>/dev/null || \
        ! diff $tmp_bridge_config $bridge_config &>/dev/null || \
        ! diff $tmp_int_public_config $int_public_config &>/dev/null || \
        ! diff $tmp_bridge_route $bridge_route &>/dev/null ; then

        ifdown int_public &>/dev/null || true
        ifdown $interface &>/dev/null || true
        ifdown $bridge &>/dev/null || true

        cp $tmp_interface_config $interface_config
        cp $tmp_bridge_config $bridge_config
        cp $tmp_bridge_route $bridge_route
        if [ -n "$public_tag" ]; then
            cp $tmp_int_public_config $int_public_config
        else
            rm -f $int_public_config
        fi

        ifup $bridge
        ifup $interface
        if [ -n "$public_tag" ]; then
            ifup int_public
        fi
    fi

    rm $tmp_bridge_config
    rm $tmp_interface_config

}

# elastic network interfaces used on Debian/Ubuntu, etc.
function configure_bridge_interface_dhcp_eni() {

    local bridge=$1
    local interface=$2
    local public_ip_addr=${3:-''}
    local public_ip_netmask=${4:-''}
    local interface_mac=${5}
    local public_tag=${6}
    local public_tag_ip=${7}
    local public_tag_ip_netmask=${8}
    local bridge_route_prefix=${9}
    local bridge_route_via=${10}
    local tmp_config=$(mktemp)
    local config="/etc/network/interfaces"

    cp $config $tmp_config
    sed -e "/auto $interface\$/,/^$/d" -i $tmp_config
    sed -e "/auto $bridge\$/,/^$/d" -i $tmp_config
    sed -e "/auto int_public\$/,/^$/d" -i $tmp_config

    if [ -n "$bridge_route_prefix" ]; then
        local route_line="post-up ip route replace $bridge_route_prefix via $bridge_route_via"
    else
        local route_line=
    fi

    ovs_ports="$interface"
    if [ -n "$public_tag" ]; then
        ovs_ports="$ovs_ports int_public"
    fi

    #bridge config
    if [ -z "$public_ip_addr" ]; then
        # DHCP for the bridge itself.
        cat >> $tmp_config <<EOF_CAT
auto $bridge
allow-ovs $bridge
iface $bridge inet dhcp
 pre-up ip addr flush dev $interface
 ovs_type OVSBridge
 ovs_ports $ovs_ports
 ovs_extra set bridge $bridge other-config:hwaddr=$interface_mac
 $route_line

EOF_CAT
    else
        cat >> $tmp_config <<EOF_CAT
auto $bridge
allow-ovs $bridge
iface $bridge inet static
 pre-up ip addr flush dev $interface
 address $public_ip_addr
 netmask $public_ip_netmask
 ovs_type OVSBridge
 ovs_extra set bridge $bridge other-config:hwaddr=$interface_mac
 $route_line

EOF_CAT
    fi
    if [ -n "$public_tag" ]; then
        # Setup the access port
        cat >> $tmp_config <<EOF_CAT
auto int_public
allow-$bridge int_public
iface int_public inet static
 address $public_tag_ip
 netmask $public_tag_ip_netmask
 ovs_bridge $bridge
 ovs_type OVSIntPort
 ovs_options tag=$public_tag
EOF_CAT
    fi

    #interface config
    cat >> $tmp_config <<EOF_CAT
auto $interface
allow-$bridge $interface
iface $interface inet manual
 ovs_bridge $bridge
 ovs_type OVSPort

EOF_CAT

    if ! diff $tmp_config $config &>/dev/null; then
        ifdown int_public &>/dev/null || true
        ifdown $interface &>/dev/null || true
        ifdown $bridge &>/dev/null || true

        cp $tmp_config $config

        ifup $bridge
        ifup $interface
        if [ -n "$public_tag" ]; then
            ifup int_public
        fi
    fi

    rm $tmp_config

}

if [ -n "$PHYSICAL_INTERFACE_IP_NETMASK" ]; then
    IP=$(python -c "import netaddr; print netaddr.IPNetwork('$PHYSICAL_INTERFACE_IP_NETMASK').ip")
    NETMASK=$(python -c "import netaddr; print netaddr.IPNetwork('$PHYSICAL_INTERFACE_IP_NETMASK').netmask")
else
    IP=''
    NETMASK=''
fi

if [ -n "$PUBLIC_TAG_IP" ]; then
    PUBLIC_TAG_IP_NETMASK=$(python -c "import netaddr; print netaddr.IPNetwork('$PUBLIC_TAG_IP').netmask")
    PUBLIC_TAG_IP=$(python -c "import netaddr; print netaddr.IPNetwork('$PUBLIC_TAG_IP').ip")
else
    PUBLIC_TAG_IP_NETMASK=
fi

interface_mac=$(ip link show dev "$PHYSICAL_INTERFACE" | awk '/ether/ {print $2}')

if [ -d "/etc/sysconfig/network-scripts/" ]; then
    configure_bridge_interface_dhcp_netscripts $EXTERNAL_BRIDGE $PHYSICAL_INTERFACE "$IP" "$NETMASK" $interface_mac "$PUBLIC_TAG" "$PUBLIC_TAG_IP" "$PUBLIC_TAG_IP_NETMASK" "$BRIDGE_ROUTE_PREFIX" "$BRIDGE_ROUTE_VIA"
elif [ -d "/etc/network" ]; then
    configure_bridge_interface_dhcp_eni $EXTERNAL_BRIDGE $PHYSICAL_INTERFACE "$IP" "$NETMASK" $interface_mac "$PUBLIC_TAG" "$PUBLIC_TAG_IP" "$PUBLIC_TAG_IP_NETMASK" "$BRIDGE_ROUTE_PREFIX" "$BRIDGE_ROUTE_VIA"
else
    echo "Unsupported network configuration type!"
    exit 1
fi

# Handle default route replacement.
if [ -n "$PUBLIC_INTERFACE_ROUTE" ]; then
    DEFAULT_VIA=$(ip route show | awk '/default / { print $3 }')
    if [ "$DEFAULT_VIA" != "$PUBLIC_INTERFACE_ROUTE" ]; then
        if [ -z "$(ip route show 169.254.169.254)" ]; then
            # No explicit route to 169.254.169.254 - set one.
            ip route add 169.254.169.254/32 via $DEFAULT_VIA
        fi
        ip route replace default via $PUBLIC_INTERFACE_ROUTE
    fi
fi
