#! /bin/bash

arg_file=$1

hexify() {
    echo $1 | awk -F . '{printf "%02X%02X%02X%02X", $1, $2, $3, $4}'
}

make_quad() {
    numeric=$1
    q1=$(( ($numeric>>24) & 0xff ))
    q2=$(( ($numeric>>16) & 0xff ))
    q3=$(( ($numeric>>8) & 0xff ))
    q4=$(( ($numeric) & 0xff ))
    echo "$q1.$q2.$q3.$q4"
}

make_context() {
    if [ $# > 0 ]
    then
	if [ "x$1" != "x" ]
	then
            echo "ip netns exec $1"
	else
	    echo ""
	fi
    else
        echo ""
    fi
}

have_device() {
    local dev=$1
    if [ $# > 1 ]
    then
	local ns=$2
    fi
    if [ "x${ns}" == "x" ]
    then
	unset -v ns
    fi
    local ctx=$(make_context $ns)
    echo "looking for ${dev} (${ctx})" >>${log}
    ${ctx} ip link show ${dev} >>${log} 2>&1
    return $?
}

# This has some Byzantine fact handling, since we store things in a
# global variable.  That means we have to move it aside when we'd be
# adding things that need to appear multiple times, so that we can
# encapsulate them.
do_full() {
    local name=$1;shift
    local ip=$1;shift
    local gw=$1;shift
    local gwdev=$1;shift
    local subnet=$1;shift
    local prefix=$1;shift
    local i_dev=$1;shift
    local e_dev=$1;shift
    local gwns=$1;shift

    local c_name="\"name\":\"${name}\""
    local c_type="\"type\":\"${type}\""
    local c_addr="\"addr\":\"${ip}\""
    local c_idev="\"internal_device\":\"${i_dev}\""
    local c_edev="\"external_device\":\"${e_dev}\""
    local ctr_info="${c_name},${c_type},${c_addr},${c_idev},${c_edev}"

    local oldfacts=${facts}
    facts=""
    do_link ${i_dev} ${ip} ${name} ${e_dev} ${gw} ${gwns}
    local retval=$?
    local linkfacts=${facts}
    facts=${oldfacts}
    local ctr_info="${ctr_info},\"link\":{${linkfacts}}"
    if [ 0 != $retval ]
    then
        add_fact "container" "{${ctr_info}}"
        return 1
    fi

    local i_ctx=$(make_context ${name})
    local e_ctx=$(make_context ${gwns})

    local oldfacts=${facts}
    facts=""
    do_route ${i_dev} ${ip} "${subnet}/${prefix}" ${name}
    local retval=$?
    local routefacts=${facts}
    facts=${oldfacts}
    local rfactsarray="[{${routefacts}}"
    if [ 0 != $retval ]
    then
        local ctr_info="${ctr_info},\"routes\":${rfactsarray}]"
        add_fact "container" "{${ctr_info}}"
        return 1
    fi
    local oldfacts=${facts}
    facts=""
    do_route ${e_dev} ${gw} ${ip}/32 ${gwns}
    local retval=$?
    local routefacts=${facts}
    facts=${oldfacts}
    local rfactsarray="${rfactsarray},{${routefacts}}]"
    local ctr_info="${ctr_info},\"routes\":${rfactsarray}"
    if [ 0 != $retval ]
    then
        add_fact "container" "{${ctr_info}}"
        return 1
    fi

    local oldfacts=${facts}
    facts=""
    do_netem ${e_dev} ${gwns}
    local retval=$?
    local netemfacts=${facts}
    facts=${oldfacts}
    local nfactsarray="[{${netemfacts}}"
    if [ 0 != $retval ]
    then
        local ctr_info="${ctr_info},\"qdiscs\":${nfactsarray}]"
        add_fact "container" "{${ctr_info}}"
        return 1
    fi
    local oldfacts=${facts}
    facts=""
    do_netem ${i_dev} ${name}
    local retval=$?
    local netemfacts=${facts}
    facts=${oldfacts}
    local nfactsarray="${nfactsarray},{${netemfacts}}]"
    local ctr_info="${ctr_info},\"qdiscs\":${nfactsarray}"
    if [ 0 != $retval ]
    then
        add_fact "container" "{${ctr_info}}"
        return 1
    fi
    if [ "x${bandwidth}" != "x" ]
    then
        ${i_ctx} tc qdisc change dev ${i_dev} root netem rate ${bandwidth} >>${log} 2>&1
        add_fact "bandwidth" "\"${bandwidth}\""
    fi

    local oldfacts=${facts}
    facts=""
    do_addif ${e_dev} ${gwdev} ${gwns}
    local retval=$?
    local addiffacts=${facts}
    facts=${oldfacts}
    local ctr_info="${ctr_info},\"bridging\":{${addiffacts}}"
    if [ 0 != $retval ]
    then
        add_fact "container" "{${ctr_info}}"
        return 1
    fi

    add_fact "container" "{${ctr_info}}"
    return 0
}

do_was() {
    local hexgw=$1
    local wprefix=$2
    local name=$3
    local i_dev=$4
    local ip=$5

    wlength=$(( 32 - ${wprefix} ))
    wsubnet=$(make_quad $(( 0x${hexgw} >> $wlength << $wlength )))
    local oldfacts=${facts}
    facts=""
    do_route ${i_dev} ${ip} "${wsubnet}/${wprefix}" ${name}
    local retval=$?
    local routefacts=${facts}
    facts=${oldfacts}
    add_fact "external_subnet" "{${routefacts}}"

    return ${retval}
}

do_link() {
    local dev0=$1
    local addr0=$2
    local ns0=$3
    local dev1=$4
    local addr1=$5
    local ns1=$6

    if [ "x${ns0}" == "x" ]
    then
        unset -v ns0
    fi
    if [ "x${ns1}" == "x" ]
    then
        unset -v ns1
    fi

    local ctx0=$(make_context ${ns0})
    local ctx1=$(make_context ${ns1})

    have_device $dev0 $ns0
    local have_dev0=$?
    have_device $dev1 $ns1
    local have_dev1=$?

    make_ep_json() {
        local json="{"
        local json="${json}\"addr\":\"$1\","
        local json="${json}\"dev\":\"$2\""
        if [ $# > 2 ]
        then
            local json="${json},\"ns\":\"$3\""
        fi
        local json="${json}}"
        echo $json
    }
    local ep0=$(make_ep_json $addr0 $dev0 $ns0)
    local ep1=$(make_ep_json $addr1 $dev1 $ns1)
    add_fact "endpoints" "[${ep0},${ep1}]"

    # Idempotent link add
    if [ \( 0 -eq ${have_dev0} \) -a \( 0 -eq ${have_dev1} \) ]
    then
        /bin/true
    elif [ 0 -eq ${have_dev0} ]
    then
        err="device ${dev0} already exists"
        add_fact "linked" "false"
        return 1
    elif [ 0 -eq ${have_dev1} ]
    then
        err="device ${dev1} already exists"
        add_fact "linked" "false"
        return 1
    else
        is_changed="true"
        ip link add ${dev0} type veth peer name ${dev1} >>${log} 2>&1
        if [ $? -ne 0 ]
        then
            err="cannot create devices ${dev0} and ${dev1}"
            return 1
        fi
        if [ "x${ns0}" != "x" ]
        then
            ip link set ${dev0} netns ${ns0} >>${log} 2>&1
            if [ $? -ne 0 ]
            then
		err="cannot put ${dev0} in namespace ${ns0}"
		return 1
            fi
        fi
        if [ "x${ns1}" != "x" ]
        then
            ip link set ${dev1} netns ${ns1} >>${log} 2>&1
            if [ $? -ne 0 ]
            then
		err="cannot put ${dev1} in namespace ${ns1}"
		return 1
            fi
        fi
    fi

    # Setting the link address is idempotent already.
    $ctx0 ip addr add $addr0/32 dev $dev0 >>${log} 2>&1
    $ctx1 ip addr add $addr1/32 dev $dev1 >>${log} 2>&1

    # Bringing up a link is idempotent already.
    $ctx0 ip link set $dev0 up >>${log} 2>&1
    $ctx1 ip link set $dev1 up >>${log} 2>&1

    add_fact "linked" "true"
    return 0
}

do_bridge() {
    local dev=$1; shift
    local addr=$1; shift
    if [ $# > 0 ]
    then
        local ns=$1; shift
    fi
    local ctx=$(make_context $ns)

    local have_bridge="no"
    local bridges=$($ctx brctl show | tail -n +1 | cut -f 1)
    for b in ${bridges}
    do
        if [ $b == $dev ]
        then
            local have_bridge="yes"
        fi
    done

    if [ ${have_bridge} == "no" ]
    then
        $ctx brctl addbr $dev >>${log} 2>&1
	$ctx ip address add dev $dev local ${addr} >>${log} 2>&1
        is_changed="true"
    fi

    # This is already idempotent
    $ctx ip link set dev $dev up >>${log} 2>&1

    local br_info="\"dev\":\"${dev}\""
    if [ "x${ns}" != "x" ]
    then
        local br_info="${br_info},\"ns\":\"${ns}\""
    fi
    add_fact "bridge" "{${br_info}}"
    return 0
}

do_addif() {
    local dev=$1
    local bridge=$2
    if [ $# > 2 ]
    then
        local ns=$3
    fi
    local ctx=$(make_context $ns)

    # Check to see if the device is already using that bridge.
    $ctx ip link show $dev | grep -E "\s${bridge}\s"
    if [ 0 -ne $? ]
    then
        $ctx brctl addif $bridge $dev >>${log} 2>&1
        is_changed="true"
    fi

    local dev_info="\"dev\":\"${dev}\""
    local bridge_info="\"bridge\":\"${bridge}\""
    if [ "x${ns}" != "x" ]
    then
        local ns_info="\"ns\":\"${ns}\""
    fi
    local if_info="${dev_info},${bridge_info}${ns_info:+,}${ns_info}"
    add_fact "addif" "{${if_info}}"
    return 0
}

do_route() {
    local dev=$1
    local addr=$2
    local net=$3
    if [ $# > 3 ]
    then
        local ns=$4
    fi
    local info_dev="\"dev\":\"${dev}\""
    local info_addr="\"from\":\"${addr}\""
    local info_net="\"to\":\"${net}\""
    if [ "x${ns}" != "x" ]
    then
        local info_ns=",\"ns\":\"${ns}\""
    fi
    local ctx=$(make_context $ns)
    $ctx ip route list match | grep $dev
    if [ 0 -ne $? ]
    then
        local msg=$($ctx ip route add $net dev $dev proto static scope global src $addr 2>&1)
        if [ 0 -ne $? ]
        then
            err="failed to add route: ${msg}"
            add_fact "route" "{${info_dev},${info_addr},${info_net}${info_ns},\"success\":false}"
            return 1
        fi
        is_changed="true"
    fi

    add_fact "route" "{${info_dev},${info_addr},${info_net}${info_ns},\"success\":true}"
    return 0
}

do_netem() {
    local dev=$1
    if [ $# > 1 ]
    then
        local ns=$2
    fi
    local ctx=$(make_context $ns)
    $ctx ip link show $dev | grep -E "\snetem\s"
    if [ 0 -ne $? ]
    then
        $ctx tc qdisc add dev $dev root handle 1:0 netem >>${log} 2>&1
        is_changed="true"
    fi

    local info_dev="\"dev\":\"${dev}\""
    if [ "x${ns}" != "x" ]
    then
        local info_ns=",\"ns\":\"${ns}\""
    fi
    add_fact "netem" "{${info_dev}${info_ns}}"

    return 0
}

add_fact() {
    local fact_name=$1
    local fact_value=$2
    facts="${facts}${facts:+,}\"${fact_name}\":${fact_value}"
}

parse_args() {
    local state="full"
    arg_file=$1
    for line in $(cat ${arg_file})
    do
        local $line
    done
    case "${state}" in
        "full")
            local hexaddr=$(hexify ${ip})
            local i_dev="ctr_i${hexaddr}" # Internal -- container-side
            local e_dev="ctr_e${hexaddr}" # External -- gateway-side

            local hexgw=$(hexify ${gw})
            local length=$(( 32 - ${prefix} ))
            local subnet=$(make_quad $(( 0x${hexgw} >> $length << $length )))

            do_full ${name} ${ip} ${gw} ${gwdev} \
                ${subnet} ${prefix} ${i_dev} ${e_dev} ${gwns}
            if [ 0 -ne $? ]
            then
                return 1
            fi
            if [ ${wprefix} -gt 0 ]
            then
                do_was ${hexgw} ${wprefix} ${name} ${i_dev} ${ip}
            else
                return 0
            fi
            ;;
        "link")
            do_link $dev0 $addr0 ${ns0:-""} $dev1 $addr1 ${ns1:-""}
            return $?
            ;;
        "bridge")
            do_bridge $dev $addr $ns
            return $?
            ;;
        "addif")
            do_addif $dev $bridge $ns
            return $?
            ;;
        "route")
            do_route $dev $addr $net $ns
            return $?
            ;;
        "netem")
            do_netem $dev $ns
            return $?
            ;;
    esac
}

create_json() {
    local failed=$1
    if [ $# > 1 ]
    then
        local msg=$2
    fi

    local json="{"
    local json="${json} \"failed\" : ${failed}"
    local json="${json}, \"changed\" : ${is_changed}"
    local json="${json}, \"data\" : { ${facts} }"
    if [ "x${msg}" != "x" ]
    then
        local json="${json}, \"msg\" : \"${msg}\""
    fi
    local json="${json} }"
    echo ${json}
}

facts=""
err=""
is_changed="false"
log=$(mktemp)
parse_args ${arg_file}
if [ 0 -eq $? ]
then
    echo $(create_json "false")
    exit 0
else
    echo $(create_json "true" "${err}")
    exit 1
fi
