#!/bin/sh
#-------------------------------------------------------------------------+
# Copyright (C) 2015 Matt Churchyard (churchers@gmail.com)
# All rights reserved
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted providing that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# create switches from rc list on init
# this should run once per boot to make sure switches from the
# configuration file have bridge interfaces. If any new switches are
# created, the bridge is done at the same time
#
switch::init(){
    local _switchlist _switch _id
    local _nat _havenat=0 _bridge _vale

    config::core_get "_switchlist" "switch_list"

    # create bridges for each switch if they don't already exist
    if [ -n "${_switchlist}" ]; then
        for _switch in ${_switchlist}; do
            # do nothing if it's a vale switch
            config::core_get "_vale" "vale_${_switch}"
            util::checkyesno "${_vale}" && continue

            # do nothing if the bridge already exists
            switch::get_ident "_id" "${_switch}"
            [ -n "${_id}" ] && continue

            config::core_get "_nat" "nat_${_switch}"
            config::core_get "_bridge" "bridge_${_switch}"

            if [ -n "${_bridge}" ]; then
                ifconfig "${_bridge}" description "vm-${_switch}" up >/dev/null 2>&1
            else
                _id=$(ifconfig bridge create)
                [ $? -ne 0 ] && util::err "failed to create bridge interface"
                ifconfig "${_id}" priority 0
                ifconfig "${_id}" description "vm-${_switch}" up

                # add all member interfaces
                switch::__add_allmembers "${_switch}" "${_id}"
            fi

            [ -n "${_nat}" ] &&_havenat="1"
        done
    fi

    # only load dnsmasq/pf if we have a nat switch
    [ "${_havenat}" = "1" ] && switch::nat_init
}

# list switches, currently just from stored configuration
#
switch::list(){
    local _switchlist _portlist _switch _port
    local _vlan _nat _bridge _type _vale
    local _id _format="%-15s %-10s %-11s %-9s %-12s %s\n"

    config::core_get "_switchlist" "switch_list"

    printf "${_format}" "NAME" "TYPE" "IDENT" "VLAN" "NAT" "PORTS"

    for _switch in ${_switchlist}; do
        switch::get_ident "_id" "${_switch}"
        [ -z "${_id}" ] && _id="-"

        config::core_get "_portlist" "ports_${_switch}"
        config::core_get "_vlan" "vlan_${_switch}"
        config::core_get "_nat" "nat_${_switch}"
        config::core_get "_bridge" "bridge_${_switch}"
        config::core_get "_vale" "vale_${_switch}"

        if util::checkyesno "${_vale}"; then
            _type="vale"
            switch::vale_id "_id" "${_switch}"
            : ${_portlist:=n/a}
            : ${_vlan:=n/a}
            : ${_nat:=n/a}
        elif [ -z "${_bridge}" ]; then
            _type="auto"
            : ${_portlist:=-}
            : ${_vlan:=-}
            : ${_nat:=-}
        else
            _type="manual"
            _portlist="n/a"
            _vlan="n/a"
            _nat="n/a"
        fi

        printf "${_format}" "${_switch}" "${_type}" "${_id}" "${_vlan}" "${_nat}" "${_portlist}"
    done
}

# 'vm switch create'
# create a new virtual switch
#
# @param string _switch name of the switch to create
#
switch::create(){
    local _switch="$1"
    local _id _curr_list _curr

    core::check_name "${_switch}" || util::err "invalid switch name - '${_name}'"
    config::core_get "_curr_list" "switch_list"

    for _curr in $_curr_list; do
        [ "${_switch}" = "${_curr}" ] && util::err "switch ${_switch} already exists"
    done

    config::core_set "switch_list" "${_switch}" "1"
    [ $? -ne 0 ] && util::err "failed to add switch to configuration file"

    _id=$(ifconfig bridge create)
    [ $? -ne 0 ] && util::err "failed to create bridge interface"
    ifconfig "${_id}" description "vm-${_switch}" up
}

# 'vm switch import'
# use an existing manually configured bridge
#
# @param string _switch name of the switch to create
# @param string _bridge name of the bridge interface to import
#
switch::import(){
    local _switch="$1"
    local _bridge="$2"
    local _exists

    [ -z "${_switch}" -o -z "${_bridge}" ] && util::usage
    core::check_name "${_switch}" || util::err "invalid switch name - '${_name}'"
    config::core_get "_curr_list" "switch_list"

    for _curr in $_curr_list; do
        [ "${_switch}" = "${_curr}" ] && util::err "switch ${_switch} already exists"
    done

    _exists=$(ifconfig | grep "^${_bridge}: ")
    [ -z "${_exists}" ] && util::err "${_bridge} does not appear to be a valid existing bridge"

    config::core_set "switch_list" "${_switch}" "1"
    config::core_set "bridge_${_switch}" "${_bridge}"
    [ $? -ne 0 ] && util::err "failed to add switch to configuration file"

    # attach our description to bridge
    # this will allow vm-bhyve to find it and attach guests
    ifconfig "${_bridge}" description "vm-${_switch}" up
}

# 'vm switch destroy'
# remove a virtual switch
#
# @param string _switch name of the switch
#
switch::remove(){
    local _switch="$1"
    local _id _nat _bridge _vale

    config::core_get "_bridge" "bridge_${_switch}"
    config::core_get "_nat" "nat_${_switch}"
    config::core_get "_vale" "vale_${_switch}"

    # if manual, leave it there and just remove from our config
    if [ -n "${_bridge}" ]; then
        ifconfig "${_bridge}" description "" >/dev/null 2>&1
        config::core_remove "switch_list" "${_switch}"
        config::core_remove "bridge_${_switch}"
        exit 0
    fi	

    if [ -z "${_vale}" ]; then
        switch::get_ident "_id" "${_switch}"
        switch::remove_allmembers "${_switch}" "${_id}"
    fi

    config::core_remove "switch_list" "${_switch}" "1"
    config::core_remove "ports_${_switch} vlan_${_switch} nat_${_switch} vale_${_switch}"

    if [ -z "${_vale}" ]; then
        if [ -n "${_id}" ]; then
            ifconfig "${_id}" destroy >/dev/null 2>&1
            [ $? -ne 0 ] && util::warn "removed configuration but failed to remove bridge"
        else
            util::warn "removed configuration but failed to remove bridge"
        fi

        # reset nat if this switch had nat
        [ -n "${_nat}" ] && switch::nat_init
    fi
}

# 'vm switch vlan'
# set vlan number for a switch
# all interfaces will be removed, vlan interfaces created, then re-added
#
# @param string _switch name of the switch
# @param int _vlan vlan number (or 0 to switch vlan off)
#
switch::vlan(){
    local _switch="$1"
    local _vlan="$2"

    [ -z "${_switch}" -o -z "${_vlan}" ] && util::usage
    switch::__check_auto "${_switch}"

    echo "${_vlan}" | egrep -qs '^[0-9]{1,4}$'
    [ $? -ne 0 ] && util::err "invalid vlan number"
    [ ${_vlan} -ge 4095 ] && util::err "invalid vlan number"

    # we need to remove everything and re-add as raw interfaces will
    # change to vlan or visa-versa
    switch::remove_allmembers "${_switch}"

    if [ "${_vlan}" = "0" ]; then
        config::core_remove "vlan_${_switch}"
    else
        config::core_set "vlan_${_switch}" "${_vlan}"
    fi

    # put interfaces back in
    config::core_load
    switch::__add_allmembers "${_switch}"
}

# physically add switch member interfaces
# this will add all configured interfaces to the bridge
# if vlan specified, each real interface will be assigned to a vlan interface
#
# @private
# @param string _switch the switch to configure
# @param optional string _id switch id (eg bridge0) if already known
#
switch::__add_allmembers(){
    local _switch="$1"
    local _id="$2"
    local _portlist _port

    if [ -z "${_id}" ]; then
        switch::get_ident "_id" "${_switch}"
        [ -z "${_id}" ] && util::err "failed to get switch id while adding members"
    fi

    config::core_get "_portlist" "ports_${_switch}"

    if [ -n "${_portlist}" ]; then
        for _port in ${_portlist}; do
            switch::__configure_port "${_switch}" "${_id}" "${_port}"
        done
    fi
}

# physically remove all configured members from a switch
#
# @private
# @param string _switch name of the switch
# @param optional string _id switch id if already known
#
switch::remove_allmembers(){
    local _switch="$1"
    local _id="$2"
    local _portlist _port

    if [ -z "${_id}" ]; then
        switch::get_ident "_id" "${_switch}"
        [ -z "${_id}" ] && util::err "failed to get switch id while removing members"
    fi

    config::core_get "_portlist" "ports_${_switch}"

    if [ -n "${_portlist}" ]; then
        for _port in ${_portlist}; do
            switch::__unconfigure_port "${_switch}" "${_id}" "${_port}"
        done
    fi
}

# physically adds a port to a virtual switch
# if vlan is configured, we need to create the relevant vlan interface first,
# then add that to the bridge
#
# @private
# @param string _switch the switch to add port to
# @param string _id the switch id
# @param string _member name of the member interface to add
#
switch::__configure_port(){
    local _switch="$1"
    local _id="$2"
    local _member="$3"
    local _vlan _vid

    config::core_get "_vlan" "vlan_${_switch}"

    if [ -n "${_vlan}" ]; then
        switch::get_ident "_vid" "vlan-${_member}-${_vlan}"

        if [ -z "${_vid}" ]; then
            _vid=$(ifconfig vlan create)
            [ $? -ne 0 ] && util::err "failed to create vlan interface"
            ifconfig "${_vid}" vlandev "${_member}" vlan "${_vlan}" description "vm-vlan-${_member}-${_vlan}" up
        fi

        ifconfig ${_id} addm ${_vid} >/dev/null 2>&1
    else
        ifconfig ${_id} addm ${_member} >/dev/null 2>&1
    fi

    [ $? -ne 0 ] && util::err "failed to add member to the virtual switch"
}

# 'vm switch add'
# configure a member interface to a "switch"
# add the interface to bridge and update switch configuration
#
# @param string _switch name of the switch
# @param string _member name of the interface to add
#
switch::add_member(){
    local _switch="$1"
    local _member="$2"
    local _id

    [ -z "${_switch}" -o -z "${_member}" ] && util::usage

    switch::__check_auto "${_switch}"
    switch::get_ident "_id" "${_switch}"
    [ -z "${_id}" ] && util::err "unable to locate virtual switch ${_id}"

    switch::__configure_port "${_switch}" "${_id}" "${_member}"
    config::core_set "ports_${_switch}" "${_member}" "1"
}

# physically remove a port from a virtual switch / bridge
#
# @private
# @param string _switch the switch to update
# @param string _id the switch id
# @param string _member the interface to remove
#
switch::__unconfigure_port(){
    local _switch="$1"
    local _id="$2"
    local _member="$3"
    local _id _vlan _vid _usage

    config::core_get "_vlan" "vlan_${_switch}"

    if [ -n "${_vlan}" ]; then
        switch::get_ident "_vid" "vlan-${_member}-${_vlan}"
        [ -z "${_vid}" ] && util::err "unable to find relevent vlan interface for ${_member}"

        ifconfig ${_id} deletem ${_vid} >/dev/null 2>&1
    else
        ifconfig ${_id} deletem ${_member} >/dev/null 2>&1
    fi

    [ $? -ne 0 ] && util::err "failed to remove member from the virtual switch"

    # it's possible a vlan interface may be assigned to multiple switches
    # we want to remove the vlan interface if possible, but not if it's
    # still assigned to another switch
    if [ -n "${_vlan}" -a -n "${_vid}" ]; then
        _usage=$(ifconfig -a |grep "member: vm-vlan-${_member}-${_vlan}\$")
        [ -z "${_usage}" ] && ifconfig "${_vid}" destroy >/dev/null 2>&1
    fi
}

# 'vm switch remove'
# remove a member interface from a switch configuration
# update configuration then remove device from bridge
#
# @param string _switch the switch to update
# @param string _member the interface to remove
#
switch::remove_member(){
    local _switch="$1"
    local _member="$2"
    local _id

    [ -z "${_switch}" -o -z "${_member}" ] && util::usage

    switch::__check_auto "${_switch}"
    switch::get_ident "_id" "${_switch}"
    [ -z "${_id}" ] && util::err "unable to locate virtual switch ${_id}"

    config::core_remove "ports_${_switch}" "${_member}"
    switch::__unconfigure_port "${_switch}" "${_id}" "${_member}"
}

# 'vm switch nat'
# configure nat on a switch
# this function just deals with the vm-bhyve configuration
#
# @param string _switch the name of the switch
# @param string _nat=on|off whether to switch it on or off
#
switch::nat(){
    local _switch="$1"
    local _nat="$2"

    [ -z "${_switch}" ] && util::usage
    switch::__check_auto "${_switch}"

    case "${_nat}" in
        off)
            config::core_remove "nat_${_switch}"
            ;;
        on)
            load_rc_config "pf"

            if ! checkyesno pf_enable; then
                util::err "pf needs to be enabled for nat functionality"
            fi

            config::core_set "nat_${_switch}" "yes"
            [ $? -ne 0 ] && util::err "failed to store nat configuration"

            echo "******"
            echo "  NAT has been enabled on the specified switch"
            echo "  A sample dnsmasq configuration has been created in /usr/local/etc/dnsmasq.conf.bhyve"
            echo "  To enable DHCP on this switch, please install the dnsmasq confguration or merge with your existing."
            echo "******"
            ;;
        *)
            util::err "last option should either be 'on' or 'off' to enable/disable nat functionality"
            ;;
    esac

    # reset nat configuration
    switch::nat_init
}

# set the system up for nat usage
# do the actual nat work
# we completely take over dnsmasq and assign it to the required interfaces
# /etc/pf.conf gets an include statement added pointing to a file in
# out .config directory. This file contains a nat rule for each nat switch
#
# @private
#
switch::nat_init(){
    local _pf_rules="${vm_dir}/.config/pf-nat.conf"
    local _havenat=0
    local _grep _switchlist _nat _net24 _if
    local _gw _bnum

    # reload configuration in case it's just been changed
    config::core_load

    # get default gateway
    _gw=$(netstat -4rn | grep default | awk '{print $4}')

    # basic dnsmasq settings
    echo "# vm-bhyve dhcp" > /usr/local/etc/dnsmasq.conf.bhyve
    echo "port=0" >> /usr/local/etc/dnsmasq.conf.bhyve
    echo "domain-needed" >> /usr/local/etc/dnsmasq.conf.bhyve
    echo "no-resolv" >> /usr/local/etc/dnsmasq.conf.bhyve
    echo "except-interface=lo0" >> /usr/local/etc/dnsmasq.conf.bhyve
    echo "bind-interfaces" >> /usr/local/etc/dnsmasq.conf.bhyve
    echo "local-service" >> /usr/local/etc/dnsmasq.conf.bhyve
    echo "dhcp-authoritative" >> /usr/local/etc/dnsmasq.conf.bhyve

    # reset our pf config and create /etc/pf.conf if needed
    echo "# vm-bhyve nat" > "${vm_dir}/.config/pf-nat.conf"
    [ ! -e "/etc/pf.conf" ] && touch /etc/pf.conf

    # only add our include statement to /etc/pf.conf if it's not already in there somwhere
    _grep=$(grep "${_pf_rules}" /etc/pf.conf)
    [ -z "${_grep}" ] && echo "include \"${_pf_rules}\"" >> /etc/pf.conf

    config::core_get "_switchlist" "switch_list"

    # add each nat switch to dnsmasq.conf and .config/pf-nat.conf
    for _switch in ${_switchlist}; do
        config::core_get "_nat" "nat_${_switch}"
        switch::get_ident "_id" "${_switch}"

        if [ "${_nat}" = "yes" -a -n "${_id}" ]; then
            _bnum=$(echo "${_id}" |awk -F'bridge' '{print $2}')
            _net24="172.16.${_bnum}"

            echo "nat on ${_gw} from {${_net24}.0/24} to any -> (${_gw})" >> "${vm_dir}/.config/pf-nat.conf"
            echo "" >> /usr/local/etc/dnsmasq.conf.bhyve
            echo "interface=${_id}" >> /usr/local/etc/dnsmasq.conf.bhyve
            echo "dhcp-range=${_net24}.10,${_net24}.254" >> /usr/local/etc/dnsmasq.conf.bhyve

            # make sure interface has an ip
            # this doesn't get removed when nat disabled but not a major issue
            ifconfig "${_id}" ${_net24}.1/24

            _havenat="1"
        fi
    done

    # make sure forwarding enabled if we have any nat
    [ "${_havenat}" = "1" ] && sysctl net.inet.ip.forwarding=1 >/dev/null 2>&1

    # restart services regardless
    # still need to restart if _havenat=0, in case we've just removed last nat switch
    util::restart_service "pf"
}

# check if a switch is a vale switch
# if so, we don't need to actually create anything.
# interfaces are created on demand when starting guests,
# and vale switches are dynamic
#
# @return int 0 is switch is vale
#
switch::is_vale(){
    local _switch="$1"

    config::core_get "_is_vale" "vale_${_switch}"
    [ -z "${_is_vale}" ] && return 1

    util::checkyesno "${_is_vale}"
}

# gets a unique port name for a vale interface
# we need to make sure vale switch name is the same
# for all interfaces on the same switch, but port is 
# different
# 
# @param string _var name of variable to put port name into
# @param string _switch the name of the switch
# @param string _port unique port identifier (usually mac address)
#
switch::vale_id(){
    local _var="$1"
    local _switch="$2"
    local _port="$3"
    local _id_s _id_p

    # get a switch id
    _id_s=$(md5 -qs "${_switch}" | awk '{print substr($0,0,5)}')

    # given port?
    if [ -n "${_port}" ]; then
        _id_p=$(md5 -qs "${_port}" | awk '{print substr($0,0,5)}')
        setvar "${_var}" "vale${_id_s}:${_id_p}"
    else
        setvar "${_var}" "vale${_id_s}"
    fi
}

# produce an error if the listed switch is not managed by vm-bhyve.
# we allow users to import existing bridges which they configure themselves.
# we don't allow vm-bhyve to mess with these, it's all up to the user
# also don't mess with vale switches
#
# @param string _switch the name of the switch
#
switch::__check_auto(){
    local _switch="$1"
    local _bridge _vale

    config::core_get "_bridge" "bridge_${_switch}"
    config::core_get "_vale" "vale_${_switch}"

    [ -n "${_bridge}" ] && util::err "this is a manual switch that is managed outside of vm-bhyve"
    [ -n "${_vale}" ] && util::err "this command is not currently supported on vale switches"
}

# get the interface name for a switch
# the id will be the name of the bridge interface (eg. "bridge0")
# ifconfig will have "description: vm-switchname" directly below the first interface line
#
# @param string _var variable to put id into
# @param string _switch the switch to look for
#
switch::get_ident(){
    local _var="$1"
    local _switch="$2"
    local _c_id

    # search ifconfig for our switch id, and pull bridge interface name from preceeding line
    _c_id=$(ifconfig -a | grep -B 1 "vm-${_switch}\$" | head -n 1 | awk -F: '{print $1}')
    setvar "${_var}" "${_c_id}"
}
