#!/bin/sh
set -e

[ -n "$WEAVE_DEBUG" ] && set -x

SCRIPT_VERSION="(unreleased version)"
if [ "$SCRIPT_VERSION" = "(unreleased version)" ] ; then
    IMAGE_VERSION=latest
else
    IMAGE_VERSION=$SCRIPT_VERSION
fi
IMAGE_VERSION=${VERSION:-$IMAGE_VERSION}

# These are potentially used for remote execs, and have defaults, so
# we introduce them here
DOCKERHUB_USER=weaveworks
BASE_EXEC_IMAGE=$DOCKERHUB_USER/weaveexec
EXEC_IMAGE=$BASE_EXEC_IMAGE:$IMAGE_VERSION

# Define some regular expressions for matching addresses.
# The regexp here is far from precise, but good enough.
IP_REGEXP="[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}"
CIDR_REGEXP="$IP_REGEXP/[0-9]{1,2}"

usage() {
    echo "Usage:"
    echo "weave setup"
    echo "weave launch       [-password <password>] [-nickname <nickname>] [-iprange <cidr>] <peer> ..."
    echo "weave launch-dns   <cidr>"
    echo "weave launch-proxy [-H <docker_endpoint>] [--with-dns] [--with-ipam]"
    echo "weave connect      <peer>"
    echo "weave forget       <peer>"
    echo "weave run          [--with-dns] [<cidr> ...] <docker run args> ..."
    echo "weave start        [<cidr> ...] <container_id>"
    echo "weave attach       [<cidr> ...] <container_id>"
    echo "weave detach       <cidr> [<cidr> ...] <container_id>"
    echo "weave dns-add      <ip_address> [<ip_address> ...] <container_id> [-h <fqdn>]"
    echo "weave dns-remove   <ip_address> [<ip_address> ...] <container_id>"
    echo "weave expose       [<cidr> ...] [-h <fqdn>]"
    echo "weave hide         [<cidr> ...]"
    echo "weave ps           [<container_id> ...]"
    echo "weave status"
    echo "weave version"
    echo "weave stop"
    echo "weave stop-dns"
    echo "weave reset"
    echo "weave rmpeer       <peer_id>"
    echo
    echo "where <peer>    is of the form <ip_address_or_fqdn>[:<port>], and"
    echo "      <cidr>    is of the form <ip_address>/<routing_prefix_length>"
    echo "      <peer_id> is a <nickname> or weave internal peer ID"
    exit 1
}

exec_remote() {
    docker $DOCKER_CLIENT_ARGS run --rm --privileged --net=host \
        -v /var/run/docker.sock:/var/run/docker.sock \
        -v /proc:/hostproc \
        -e PROCFS=/hostproc \
        -e VERSION \
        -e WEAVE_DEBUG \
        -e WEAVE_DOCKER_ARGS \
        -e WEAVEDNS_DOCKER_ARGS \
        -e WEAVEPROXY_DOCKER_ARGS \
        -e WEAVE_PASSWORD \
        -e WEAVE_PORT \
        -e WEAVE_CONTAINER_NAME \
        -e DOCKER_BRIDGE \
        $WEAVEEXEC_DOCKER_ARGS $EXEC_IMAGE --local "$@"
}

check_docker_version() {
    if ! DOCKER_VERSION=$(docker -v | sed -n -e 's|^Docker version \([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*|\1|p') || [ -z "$DOCKER_VERSION" ] ; then
        echo "ERROR: Unable to parse docker version" >&2
        exit 1
    fi

    # guard against https://github.com/docker/docker/issues/8632
    if [ "$DOCKER_VERSION" = "1.3.0" ] ; then
        echo "You are running Docker version $DOCKER_VERSION, which contains a bug that prevents weave from working properly. Please upgrade." >&2
        exit 1
    fi
}

is_cidr() {
    echo "$1" | grep -E "^$CIDR_REGEXP$" >/dev/null
}

validate_cidr() {
    if ! is_cidr "$1" ; then
        echo "Invalid address prefix: $1" >&2
        echo "Must be in CIDR notation <ip_address>/<prefix_length>, e.g. 10.2.1.1/24" >&2
        exit 1
    fi
}

collect_cidr_args() {
    CIDR_ARGS=""
    CIDR_COUNT=0
    while is_cidr "$1"; do
        CIDR_ARGS="$CIDR_ARGS $1"
        CIDR_COUNT=$((CIDR_COUNT + 1))
        shift 1
    done
}

is_ip() {
    echo "$1" | grep -E "^$IP_REGEXP$" >/dev/null
}

validate_ip() {
    if ! is_ip "$1" ; then
        echo "Invalid IP: $1" >&2
        echo "IP must be of the form <aaa.bbb.ccc.ddd>, e.g. 10.2.1.1" >&2
        exit 1
    fi
}

collect_ip_args() {
    IP_ARGS=""
    IP_COUNT=0
    while is_ip "$1"; do
        IP_ARGS="$IP_ARGS $1"
        IP_COUNT=$((IP_COUNT + 1))
        shift 1
    done
    if [ $IP_COUNT -eq 0 ]; then
        [ $# -gt 0 ] && validate_ip "$1" || usage
    fi
}

# "run" is a special case because we want to use docker directly,
# rather than the docker in $EXEC_IMAGE remotely. That's because we
# are passing arbitrary arguments on to docker run, and we can't rely
# on our baked-in docker to support those arguments.
if [ "$1" = "run" ]; then
    shift 1
    check_docker_version
    if [ "$1" = "--with-dns" ]; then
        shift 1
        DNS_ARGS=$(exec_remote dns-args "$@")
    fi
    collect_cidr_args "$@"
    shift $CIDR_COUNT
    CONTAINER=$(docker $DOCKER_CLIENT_ARGS run $DNS_ARGS -d "$@")
    exec_remote attach $CIDR_ARGS $CONTAINER
    echo $CONTAINER
    exit 0
elif [ "$1" != "--local" ]; then
    exec_remote "$@"
    exit $?
fi

shift 1

BASE_IMAGE=$DOCKERHUB_USER/weave
BASE_DNS_IMAGE=$DOCKERHUB_USER/weavedns
IMAGE=$BASE_IMAGE:$IMAGE_VERSION
DNS_IMAGE=$BASE_DNS_IMAGE:$IMAGE_VERSION

PROCFS=${PROCFS:-/proc}
DOCKER_BRIDGE=${DOCKER_BRIDGE:-docker0}
CONTAINER_NAME=${WEAVE_CONTAINER_NAME:-weave}
DNS_CONTAINER_NAME=weavedns
BRIDGE=weave
CONTAINER_IFNAME=ethwe
MTU=65535
PORT=${WEAVE_PORT:-6783}
HTTP_PORT=6784
DNS_HTTP_PORT=6785
PROXY_PORT=12375
PROXY_CONTAINER_NAME=weaveproxy
PROXY_HOST="localhost"
if [ "$DOCKER_HOST" != "${DOCKER_HOST#tcp://}" ] ; then
    PROXY_HOST=$(echo "$DOCKER_HOST" | sed -e "s|tcp://\([^:]*\):[0-9]*|\1|")
fi


######################################################################
# general helpers; independent of docker and weave
######################################################################

# utility function to check whether a command can be executed by the shell
# see http://stackoverflow.com/questions/592620/how-to-check-if-a-program-exists-from-a-bash-script
command_exists () {
    command -v $1 >/dev/null 2>&1
}

run_iptables() {
    # -w is recent addition to iptables
    if [ -z "$CHECKED_IPTABLES_W" ] ; then
        if iptables -S -w >/dev/null 2>&1 ; then
            IPTABLES_W=-w
        fi
        CHECKED_IPTABLES_W=1
    fi

    iptables $IPTABLES_W "$@"
}

# Add a rule to iptables, if it doesn't exist already
add_iptables_rule() {
    IPTABLES_TABLE="$1"
    shift 1
    if ! run_iptables -t $IPTABLES_TABLE -C "$@" >/dev/null 2>&1
    then
        run_iptables -t $IPTABLES_TABLE -A "$@" >/dev/null
    fi
}

# Delete a rule from iptables, if it exist
delete_iptables_rule() {
    IPTABLES_TABLE="$1"
    shift 1
    if run_iptables -t $IPTABLES_TABLE -C "$@" >/dev/null 2>&1
    then
        run_iptables -t $IPTABLES_TABLE -D "$@" >/dev/null
    fi
}

# Configure the ARP cache parameters for the given interface.  This
# makes containers react more quickly to a change in the MAC address
# associated with an IP address.
configure_arp_cache() {
    $2 sh -c "echo 5 >/proc/sys/net/ipv4/neigh/$1/base_reachable_time &&
              echo 2 >/proc/sys/net/ipv4/neigh/$1/delay_first_probe_time &&
              echo 1 >/proc/sys/net/ipv4/neigh/$1/ucast_solicit"
}

# Send out an unsolicated ARP response to update ARP cache entries
# accross the weave network.  We do this in addition to
# configure_arp_cache because a) with those ARP cache settings it
# still takes a few seconds to correct a stale ARP mapping, and b)
# there is a kernel bug that means that the base_reachable_time
# setting is not promptly obeyed
# (<https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=4bf6980dd0328530783fd657c776e3719b421d30>>).
arp_update() {
    # It's not the end of the world if this doesn't run - we configure
    # ARP caches so that stale entries will be noticed quickly.
    if command_exists arping ; then
        $3 arping -U -q -I $1 -c 1 ${2%/*}
    fi
}

# Generate a random MAC value
random_mac() {
    # In the first byte of the MAC, the 'multicast' bit should be
    # clear and 'locally administered' bit should be set.  All other
    # bits should be random.
    od -txC -An -N6 /dev/urandom | sed 's|^ ||;s| |:|;s|[0-3]:|2:|;s|[4-7]:|6:|;s|[89ab]:|a:|;s|[c-f]:|e:|;s| |:|g'
}

######################################################################
# weave and docker specific helpers
######################################################################

create_bridge() {
    [ ! -d /sys/class/net/$BRIDGE ] && {
        ip link add name $BRIDGE type bridge
        # Set a random MAC address on the bridge.  Current Linux
        # kernels already do this when creating a bridge, but there
        # are rumours it was not always so.
        ip link set dev $BRIDGE address $(random_mac)
        # Attempting to set the bridge MTU to a high value directly
        # fails. Bridges take the lowest MTU of their interfaces. So
        # instead we create a temporary interface with the desired
        # MTU, attach that to the bridge, and then remove it again.
        ip link add name v${CONTAINER_IFNAME}du mtu $MTU type dummy
        ip link set dev v${CONTAINER_IFNAME}du master $BRIDGE
        ip link del dev v${CONTAINER_IFNAME}du
        # Drop traffic from Docker bridge to Weave; it can break subnet isolation
        if [ "$DOCKER_BRIDGE" != "$BRIDGE" ] ; then
            # Note using -I to insert ahead of Docker's bridge rules
            run_iptables -t filter -I FORWARD -i $DOCKER_BRIDGE -o $BRIDGE -j DROP
        fi
        # Work around the situation where there are no rules allowing traffic
        # across our bridge. E.g. ufw
        add_iptables_rule filter FORWARD -i $BRIDGE -o $BRIDGE -j ACCEPT
        # create a chain for masquerading
        run_iptables -t nat -N WEAVE >/dev/null 2>&1 || true
        add_iptables_rule nat POSTROUTING -j WEAVE
    }
    if [ ! "$1" = "--without-ethtool" ] ; then
        ethtool -K $BRIDGE tx off >/dev/null
    fi

    ip link set dev $BRIDGE up

    # Configure the ARP cache parameters on the bridge interface for
    # the sake of 'weave expose'
    configure_arp_cache $BRIDGE
}

destroy_bridge() {
    if [ -d /sys/class/net/$BRIDGE ] ; then
        ip link del dev $BRIDGE
    fi
    if [ "$DOCKER_BRIDGE" != "$BRIDGE" ] ; then
        run_iptables -t filter -D FORWARD -i $DOCKER_BRIDGE -o $BRIDGE -j DROP 2>/dev/null || true
    fi
    run_iptables -t filter -D FORWARD -i $BRIDGE -o $BRIDGE -j ACCEPT 2>/dev/null || true
    run_iptables -t nat -F WEAVE >/dev/null 2>&1 || true
    run_iptables -t nat -D POSTROUTING -j WEAVE >/dev/null 2>&1 || true
    run_iptables -t nat -X WEAVE >/dev/null 2>&1 || true
}

docker_bridge_ip() {
    DOCKER_BRIDGE_IP=$(ip -4 addr show dev $DOCKER_BRIDGE | grep -m1 -o 'inet [.0-9]*')
    DOCKER_BRIDGE_IP=${DOCKER_BRIDGE_IP#inet }
}

# the following borrows from https://github.com/jpetazzo/pipework

# Set $CONTAINER_PID to the PID of container $1, $LOCAL_IFNAME and
# $GUEST_IFNAME to suitable names for two ends of a veth pair, specific
# to the container, and execute args $2 $3 ... as a command. If an error
# is caused by container dying, swallow output from error.
with_container_netns () {
    CONTAINER="$1"
    CONTAINER_PID=$(docker inspect --format='{{.State.Pid}}' $CONTAINER)

    if [ "$CONTAINER_PID" = 0 ] ; then
        echo "Container $CONTAINER not running." >&2
        exit 1
    fi

    if [ "$CONTAINER_PID" = "<no value>" ] ; then
        echo "Container $CONTAINER unknown to Docker." >&2
        exit 1
    fi

    LOCAL_IFNAME="v${CONTAINER_IFNAME}pl${CONTAINER_PID}"
    GUEST_IFNAME="v${CONTAINER_IFNAME}pg${CONTAINER_PID}"
    IP_TMPOUT=/tmp/weave_ip_out_$$
    IP_TMPERR=/tmp/weave_ip_err_$$
    rm -f $IP_TMPOUT $IP_TMPERR

    # Run the wrapped command
    STATUS=0
    shift 1
    if ! "$@" >$IP_TMPOUT 2>$IP_TMPERR ; then
        STATUS=1
        if ! kill -0 "$CONTAINER_PID" 2>/dev/null ; then
            echo "Container $CONTAINER died" >&2
        else
            echo "Failure during network configuration for container $CONTAINER:" >&2
            cat $IP_TMPERR >&2
        fi
    else
        cat $IP_TMPOUT
        cat $IP_TMPERR >&2
    fi
    rm -f $IP_TMPOUT $IP_TMPERR
    return $STATUS
}

# Execute arguments as a command within the network namespace of $CONTAINER_PID
netnsenter() {
    nsenter --net=$PROCFS/$CONTAINER_PID/ns/net "$@"
}

connect_container_to_bridge() {
    if [ -h "$PROCFS/$CONTAINER_PID/ns/net" -a -h "$PROCFS/$$/ns/net" -a "$(readlink $PROCFS/$CONTAINER_PID/ns/net)" = "$(readlink $PROCFS/$$/ns/net)" ] ; then
        echo "Container is running in the host network namespace, and therefore cannot be" >&2
        echo "connected to weave. Perhaps the container was started with --net=host." >&2
        return 1
    fi
    ip link add name $LOCAL_IFNAME mtu $MTU type veth peer name $GUEST_IFNAME mtu $MTU || return 1

    if ! ethtool -K $GUEST_IFNAME tx off >/dev/null ||
        ! ip link set $LOCAL_IFNAME master $BRIDGE ||
        ! ip link set $LOCAL_IFNAME up ||
        ! ip link set $GUEST_IFNAME netns $PROCFS/$CONTAINER_PID/ns/net ; then
        # failed before we assigned the veth to the container's
        # namespace
        ip link del $LOCAL_IFNAME type veth || true
        return 1
    fi

    if ! netnsenter ip link set $GUEST_IFNAME name $CONTAINER_IFNAME ||
       ! configure_arp_cache $CONTAINER_IFNAME "netnsenter" ; then
        return 1
    fi
}

ask_version() {
    if ! DOCKERIMAGE=$(docker inspect --format='{{.Image}}' $1 2>/dev/null) ; then
            if ! DOCKERIMAGE=$(docker inspect --format='{{.Id}}' $2 2>/dev/null) ; then
                echo "Unable to find $2 image." >&2
            fi
    fi
    [ -n "$DOCKERIMAGE" ] && docker run --rm $DOCKERIMAGE --version
}

######################################################################
# functions invoked through with_container_netns
######################################################################

launch() {
    if ! netnsenter ip link show eth0 >/dev/null ; then
        echo "Perhaps you are running the docker daemon with container networking disabled (-b=none)." >&2
        return 1
    fi
    connect_container_to_bridge &&
        netnsenter ethtool -K eth0 tx off >/dev/null &&
        netnsenter ip link set $CONTAINER_IFNAME up
}

attach() {
    if netnsenter ip link show $CONTAINER_IFNAME >/dev/null 2>&1 ; then
        for ADDR; do
            # container already has the expected network interface, so assume we set it up already;
            # just add the IP address.
            if netnsenter ip addr show dev $CONTAINER_IFNAME | grep -F $ADDR >/dev/null ; then
                # address was there already
                continue
            fi
            netnsenter ip addr add $ADDR dev $CONTAINER_IFNAME || return 1
        done

        return 0
    fi

    connect_container_to_bridge || return 1

    for ADDR; do
        netnsenter ip addr add $ADDR dev $CONTAINER_IFNAME || return 1
    done

    netnsenter ip link set $CONTAINER_IFNAME up || return 1

    # Route multicast packets across the weave network.
    if ! netnsenter ip route show | grep '^224\.0\.0\.0/4' >/dev/null ; then
        netnsenter ip route add 224.0.0.0/4 dev $CONTAINER_IFNAME
    fi

    arp_update $CONTAINER_IFNAME $1 "netnsenter"
}

detach() {
    netnsenter ip link show $CONTAINER_IFNAME >/dev/null 2>&1 || return 0

    for ADDR; do
        if ! netnsenter ip addr show dev $CONTAINER_IFNAME | grep -F $ADDR >/dev/null ; then
            # address is not there, leave the device alone
            continue
        fi
        netnsenter ip addr del $ADDR dev $CONTAINER_IFNAME || return 1
    done

    if [ -n "$(netnsenter ip -f inet addr show dev $CONTAINER_IFNAME)" ] ; then
        # other addresses are left, leave the device alone
        return 0
    fi

    # Deleting the interface will delete the multicast route we set up
    netnsenter ip link del $CONTAINER_IFNAME type veth
}

container_weave_addrs() {
    netnsenter ip addr show dev $CONTAINER_IFNAME
}

######################################################################
# functions for interacting with containers
######################################################################

# Check that a container named $1 with image $2 is not running
check_not_running() {
    case $(docker inspect --format='{{.State.Running}} {{.Config.Image}}' $1 2>/dev/null) in
        "true $2")
            echo "$1 is already running." >&2
            exit 1
            ;;
        "true $2:"*)
            echo "$1 is already running." >&2
            exit 1
            ;;
        "false $2")
            docker rm $1 >/dev/null
            ;;
        "false $2:"*)
            docker rm $1 >/dev/null
            ;;
        true*)
            echo "Found another running container named '$1'. Aborting." >&2
            exit 1
            ;;
        false*)
            echo "Found another container named '$1'. Aborting." >&2
            exit 1
            ;;
    esac
}

http_call_ip() {
    ip="$1"
    port="$2"
    http_verb="$3"
    url="$4"
    shift 4
    curl --connect-timeout 3 -s -X $http_verb "$@" http://$ip:$port$url
}

container_ip() {
    if ! status=$(docker inspect --format='{{.State.Running}} {{.NetworkSettings.IPAddress}}' $1 2>/dev/null); then
        echo "$2" >&2
        return 1
    fi
    case "$status" in
        "true ")
            echo "$1 container has no IP address; is Docker networking enabled?" >&2
            return 1
            ;;
        true*)
            CONTAINER_IP="${status#true }"
            ;;
        *)
            echo "$3" >&2
            return 1
            ;;
    esac
}

# Call url $4 with http verb $3 on container $1 at port $2
http_call() {
    container_ip $1 \
        "$1 container is not present. Have you launched it?" \
        "$1 container is not running." \
        || return 1
    shift 1
    http_call_ip $CONTAINER_IP "$@"
}

# Wait until container $1 on port $2 is alive enough to respond to its status call
wait_for_status() {
    WAIT_TIME=0
    while [ $WAIT_TIME -lt 3 ]; do
        sleep $WAIT_TIME
        container_ip $1 \
            "$1 container has died." \
            "$1 container has died." \
            || return 1
        http_call_ip $CONTAINER_IP $2 GET /status >/dev/null && return 0
        WAIT_TIME=$((WAIT_TIME+1))
    done
    echo "Timed out waiting for $1 container to start." >&2
    return 1
}

# Just like wait_for_status, but takes the IP instead of a container ID
wait_for_status_ip() {
    WAIT_TIME=0
    while [ $WAIT_TIME -lt 3 ]; do
        sleep $WAIT_TIME
        http_call_ip $1 $2 GET ${3:-/status} >/dev/null && return 0
        WAIT_TIME=$((WAIT_TIME+1))
    done
    echo "Timed out waiting for container to start." >&2
    return 1
}

# Call $1 for all containers, passing container ID, all MACs and all IPs
with_container_addresses() {
    COMMAND=$1
    shift 1
    for CONTAINER_ID in "$@" ; do
        if [ "$CONTAINER_ID" = "weave:expose" ]; then
            ADDRS_CMD="ip addr show dev $BRIDGE"
        else
            ADDRS_CMD="with_container_netns $CONTAINER_ID container_weave_addrs"
        fi
        if CONTAINER_ADDRS=$($ADDRS_CMD 2>/dev/null) ; then
            CONTAINER_MAC=$(echo "$CONTAINER_ADDRS" | grep -o 'link/ether .*' | cut -d ' ' -f 2)
            CONTAINER_IPS=$(echo "$CONTAINER_ADDRS" | grep -o 'inet .*'       | cut -d ' ' -f 2)
            $COMMAND "$CONTAINER_ID" "$CONTAINER_MAC" "$CONTAINER_IPS"
        fi
    done
}

echo_addresses() {
    echo $1 $2 $3
}

######################################################################
# weaveDNS helpers
######################################################################

dns_args() {
    if [ \( "$DOCKER_VERSION_MAJOR" -lt 1 \) -o \
        \( "$DOCKER_VERSION_MAJOR" -eq 1 -a \
        "$DOCKER_VERSION_MINOR" -lt 3 \) ] ; then
        # Versions prior to 1.2.0 cannot parse `--dns-search=.`, and
        # versions prior to 1.3.0 fail to include the domain when
        # setting the hostname, so lookups for non-fully-qualified
        # names fail.
        echo "ERROR: The '--with-dns' option requires Docker 1.3.0 or later; you are running $DOCKER_VERSION" >&2
        exit 1
    fi
    docker_bridge_ip
    DNS_ARG="--dns $DOCKER_BRIDGE_IP"
    DNS_SEARCH_ARG="--dns-search=."
    for arg; do
        case $arg in
            --dns-search=*)
                DNS_SEARCH_ARG=""
                ;;
            *)
                ;;
        esac;
    done
    DNS_ARGS="$DNS_ARG $DNS_SEARCH_ARG"
}

# Perform operation $1 on local DNS database, for container ID $2
# with an FQDN obtained from the container's configured hostname and
# domain, at addresses $3, $4 ...
tell_dns() {
    METHOD="$1"
    CONTAINER_ID="$2"
    shift 2
    CONTAINER_FQDN=$(docker inspect --format='{{.Config.Hostname}}.{{.Config.Domainname}}.' $CONTAINER_ID 2>/dev/null) || true
    tell_dns_fqdn $METHOD $CONTAINER_ID $CONTAINER_FQDN $@
}

# Perform operation $1 on local DNS database, for container ID $2
# with FQDN $3, at addresses $4, $5 ... This function is only called
# where we know $2 is a valid container name
tell_dns_fqdn() {
    METHOD="$1"
    CONTAINER_ID="$2"
    CONTAINER_FQDN="$3"
    shift 3
    if ! status=$(docker inspect --format='{{.State.Running}}' $DNS_CONTAINER_NAME 2>/dev/null) || [ "$status" != "true" ] ; then
        # weavedns not running - silently return
        return
    fi
    # get the long form of the container ID
    CONTAINER=$(docker inspect --format='{{.Id}}' $CONTAINER_ID 2>/dev/null)
    MORE_ARGS="--data-urlencode fqdn=$CONTAINER_FQDN"
    for ADDR; do
        http_call $DNS_CONTAINER_NAME $DNS_HTTP_PORT $METHOD /name/$CONTAINER/${ADDR%/*} $MORE_ARGS || true
    done
}

# Tell the newly-started weaveDNS about existing weave IPs
populate_dns() {
    if ! wait_for_status $DNS_CONTAINER_NAME $DNS_HTTP_PORT ; then
        echo "If running, it will not be pre-populated." >&2
        return 0
    fi
    for CONTAINER in $(docker ps -q --no-trunc); do
        MORE_ARGS=$(docker inspect --format='--data-urlencode fqdn={{.Config.Hostname}}.{{.Config.Domainname}}.' $CONTAINER 2>/dev/null) || true
        if CONTAINER_IPS=$(with_container_netns $CONTAINER container_weave_addrs 2>&1 | sed -n -e 's/inet \([^/]*\)\/\(.*\)/\1/p') ; then
            for IP in $CONTAINER_IPS; do
                # NB: CONTAINER_IP is the IP of the weavedns
                # container; it is set by wait_for_status.
                if ! http_call_ip $CONTAINER_IP $DNS_HTTP_PORT PUT /name/$CONTAINER/$IP $MORE_ARGS ; then
                    echo "Failed to fully populate DNS."
                    return 1
                fi
            done
        fi
    done
}

######################################################################
# IP Allocation Management helpers
######################################################################

# Claim addresses for a container in IPAM.  Expects to be called from
# with_container_addresses.
ipam_claim() {
    for CIDR in $3; do
        # NB: CONTAINER_IP is the IP of the weave container; it is set by wait_for_status.
        http_call_ip $CONTAINER_IP $HTTP_PORT PUT /ip/$1/${CIDR%/*}
    done
}

# If we don't already have any CIDRs passed from the command-line, get one from IPAM
# $1 is the name of the container and $2, if set, is the ID to use inside IPAM
get_ipam_cidr() {
    if [ -n "$CIDR_ARGS" ] ; then
        return 0
    fi
    if [ -z "$2" ] ; then
        # get the long form of the container ID
        if ! status=$(docker inspect --format='{{.State.Running}} {{.Id}}' $1 2>/dev/null); then
            echo "Error: No such container: $1" >&2
            return 1
        fi
        case "$status" in
            true*)
                CONTAINER="${status#true }"
                ;;
            *)
                echo "$1 container is not running." >&2
                return 1
                ;;
        esac
    else
        CONTAINER="$2"
    fi
    CIDR_ARGS="$(http_call $CONTAINER_NAME $HTTP_PORT POST /ip/$CONTAINER)"
    if [ "$CIDR_ARGS" = "404 page not found" ] ; then
        echo "No IP address supplied (use the -iprange option on 'weave launch' to enable IP address allocation)" >&2
        return 1
    fi
}

##########################################################################################

[ $(id -u) = 0 ] || {
    echo "weave must be run as 'root' when run locally" >&2
    exit 1
}

uname -s -r | sed -n -e 's|^\([^ ]*\) \([0-9][0-9]*\)\.\([0-9][0-9]*\).*|\1 \2 \3|p' | {
    if ! read sys maj min ; then
        echo "ERROR: Unable to parse operating system version $(uname -s -r)" >&2
        exit 1
    fi

    if [ "$sys" != 'Linux' ] ; then
        echo "ERROR: Operating systems other than Linux are not supported (you have $(uname -s -r))" >&2
        exit 1
    fi

    if ! [ \( "$maj" -eq 3 -a "$min" -ge 8 \) -o "$maj" -gt 3 ] ; then
        echo "WARNING: Linux kernel version 3.8 or newer is required (you have ${maj}.${min})" >&2
    fi
}

if ! command_exists ip ; then
    echo "ERROR: ip utility is missing. Please install it." >&2
    exit 1
fi

if ! ip netns list >/dev/null 2>&1 ; then
    echo "ERROR: $(ip -V) does not support network namespaces." >&2
    echo "       Please install iproute2-ss111010 or later." >&2
    exit 1
fi

check_docker_version
DOCKER_VERSION_MAJOR=$(echo "$DOCKER_VERSION" | cut -d. -f 1)
DOCKER_VERSION_MINOR=$(echo "$DOCKER_VERSION" | cut -d. -f 2)
DOCKER_VERSION_PATCH=$(echo "$DOCKER_VERSION" | cut -d. -f 3)

[ $# -gt 0 ] || usage
COMMAND=$1
shift 1

case "$COMMAND" in
    setup)
        for img in $IMAGE $DNS_IMAGE $EXEC_IMAGE ; do
            docker pull $img
        done
        ;;
    # intentionally undocumented since it assumes knowledge of weave
    # internals
    create-bridge)
        create_bridge --without-ethtool
        ;;
    launch)
        check_not_running $CONTAINER_NAME $BASE_IMAGE
        create_bridge
        # We set the router name to the bridge mac since that is
        # stable across re-creations of the containers.
        #
        # TODO use the mac of one of the physical host interfaces
        # (eth0, wlan0, etc) so the name becomes stable across host
        # restarts.
        PEERNAME=$(cat /sys/class/net/$BRIDGE/address)
        # backward compatibility...
        if is_cidr "$1" ; then
            echo "WARNING: $1 parameter ignored; 'weave launch' no longer takes a CIDR as the first parameter" >&2
            shift 1
        fi
        CONTAINER_PORT=$PORT
        while true; do
            case "$1" in
                -password)
                    [ $# -gt 1 ] || usage
                    WEAVE_PASSWORD="$2"
                    export WEAVE_PASSWORD
                    shift 2
                    ;;
                -port)
                    [ $# -gt 1 ] || usage
                    CONTAINER_PORT="$2"
                    shift 2
                    ;;
                -iprange)
                    [ $# -gt 1 ] || usage
                    validate_cidr $2
                    IPRANGE="-iprange $2"
                    shift 2
                    ;;
                *)
                    break
                    ;;
            esac
        done
        # Set WEAVE_DOCKER_ARGS in the environment in order to supply
        # additional parameters, such as resource limits, to docker
        # when launching the weave container.
        CONTAINER=$(docker run --privileged -d --name=$CONTAINER_NAME \
            -p $PORT:$CONTAINER_PORT/tcp -p $PORT:$CONTAINER_PORT/udp -e WEAVE_PASSWORD -v /var/run/docker.sock:/var/run/docker.sock \
            $WEAVE_DOCKER_ARGS $IMAGE -iface $CONTAINER_IFNAME -port $CONTAINER_PORT -name "$PEERNAME" -nickname "$(hostname)" $IPRANGE "$@")
        with_container_netns $CONTAINER launch >/dev/null

        wait_for_status $CONTAINER_NAME $HTTP_PORT
        if [ -n "$IPRANGE" ] ; then
            # Tell the newly-started weave IP allocator about existing weave IPs
            with_container_addresses ipam_claim weave:expose $(docker ps -q)
        fi

        echo $CONTAINER
        ;;
    launch-dns)
        [ $# -gt 0 ] || usage
        CIDR=$1
        validate_cidr $CIDR
        shift 1
        check_not_running $DNS_CONTAINER_NAME $BASE_DNS_IMAGE
        create_bridge
        docker_bridge_ip
        # Set WEAVEDNS_DOCKER_ARGS in the environment in order to supply
        # additional parameters, such as resource limits, to docker
        # when launching the weave container.
        DNS_CONTAINER=$(docker run --privileged -d --name=$DNS_CONTAINER_NAME \
            -p $DOCKER_BRIDGE_IP:53:53/udp -v /var/run/docker.sock:/var/run/docker.sock \
            $WEAVEDNS_DOCKER_ARGS $DNS_IMAGE -iface $CONTAINER_IFNAME "$@")
        with_container_netns $DNS_CONTAINER attach $CIDR >/dev/null
        populate_dns
        echo $DNS_CONTAINER
        ;;
    launch-proxy)
        check_not_running $PROXY_CONTAINER_NAME $EXEC_IMAGE
        # Set WEAVEPROXY_DOCKER_ARGS in the environment in order to supply
        # additional parameters, such as resource limits, to docker
        # when launching the weaveproxy container.
        PROXY_CONTAINER=$(docker run --privileged --net=host -d \
            --name=$PROXY_CONTAINER_NAME \
            -p $PROXY_PORT:$PROXY_PORT/tcp \
            -v /var/run/docker.sock:/var/run/docker.sock \
            -v /proc:/hostproc -e PROCFS=/hostproc \
            --entrypoint=/home/weave/weaveproxy \
            $WEAVEPROXY_DOCKER_ARGS \
            $EXEC_IMAGE "$@")

        wait_for_status_ip $PROXY_HOST $PROXY_PORT /weave
        echo $PROXY_CONTAINER
        ;;
    connect)
        [ $# -eq 1 ] || usage
        http_call $CONTAINER_NAME $HTTP_PORT POST /connect -d "peer=$1"
        ;;
    forget)
        [ $# -eq 1 ] || usage
        http_call $CONTAINER_NAME $HTTP_PORT POST /forget -d "peer=$1"
        ;;
    status)
        http_call $CONTAINER_NAME $HTTP_PORT GET /status || true
        echo
        http_call $DNS_CONTAINER_NAME $DNS_HTTP_PORT GET /status 2>/dev/null || true
        ;;
    ps)
        [ $# -eq 0 ] && CONTAINERS="weave:expose $(docker ps -q)" || CONTAINERS="$@"
        with_container_addresses echo_addresses $CONTAINERS
        ;;
    version)
        [ $# -eq 0 ] || usage
        echo weave script $SCRIPT_VERSION
        ask_version $CONTAINER_NAME $IMAGE || true
        ask_version $DNS_CONTAINER_NAME $DNS_IMAGE || true
        if ! EXEC_IMAGE_ID=$(docker inspect --format='{{.Id}}' $EXEC_IMAGE 2>/dev/null) ; then
            echo "Unable to find $EXEC_IMAGE image." >&2
        else
            EXEC_VERSION=$(docker images --no-trunc | grep $EXEC_IMAGE_ID | grep -v latest | tr -s ' ' | cut -d ' ' -f 2 | tr "\n" ' ')
            if [ -n "$EXEC_VERSION" ] ; then
                echo "weave exec $EXEC_VERSION"
            else
                echo "weave exec (unreleased version)"
            fi
        fi
        ;;
    run)
        [ $# -gt 0 ] || usage
        if [ "$1" = "--with-dns" ] ; then
            shift 1
            dns_args "$@"
        fi
        collect_cidr_args "$@"
        shift $CIDR_COUNT
        CONTAINER=$(docker run $DNS_ARGS -d "$@")
        create_bridge
        get_ipam_cidr $CONTAINER $CONTAINER
        with_container_netns $CONTAINER attach $CIDR_ARGS >/dev/null
        tell_dns PUT $CONTAINER $CIDR_ARGS
        echo $CONTAINER
        ;;
    dns-args)
        dns_args "$@"
        echo -n $DNS_ARGS
        ;;
    docker-bridge-ip)
        docker_bridge_ip
        echo -n $DOCKER_BRIDGE_IP
        ;;
    start)
        collect_cidr_args "$@"
        shift $CIDR_COUNT
        [ $# -eq 1 ] || usage
        CONTAINER_ID="$1"
        create_bridge
        CONTAINER=$(docker start $CONTAINER_ID)
        get_ipam_cidr $CONTAINER_ID $CONTAINER
        with_container_netns $CONTAINER_ID attach $CIDR_ARGS >/dev/null
        tell_dns PUT $CONTAINER_ID $CIDR_ARGS
        echo $CONTAINER
        ;;
    attach)
        collect_cidr_args "$@"
        shift $CIDR_COUNT
        [ $# -eq 1 ] || usage
        CONTAINER_ID="$1"
        create_bridge
        get_ipam_cidr $CONTAINER_ID
        with_container_netns $CONTAINER_ID attach $CIDR_ARGS >/dev/null
        tell_dns PUT $CONTAINER_ID $CIDR_ARGS
        ;;
    detach)
        collect_cidr_args "$@"
        shift $CIDR_COUNT
        [ $# -eq 1 ] || usage
        CONTAINER_ID="$1"
        with_container_netns $CONTAINER_ID detach $CIDR_ARGS >/dev/null
        tell_dns DELETE $CONTAINER_ID $CIDR_ARGS
        ;;
    dns-add)
        collect_ip_args "$@"
        shift $IP_COUNT
        [ $# -ge 1 ] || usage
        CONTAINER_ID="$1"
        if [ $# -eq 1 ]; then
            tell_dns PUT $CONTAINER_ID $IP_ARGS
        else
            [ $# -eq 3 -a "$2" = "-h" ] || usage
            FQDN="$3"
            tell_dns_fqdn PUT $CONTAINER_ID $FQDN $IP_ARGS
        fi
        ;;
    dns-remove)
        collect_ip_args "$@"
        shift $IP_COUNT
        [ $# -eq 1 ] || usage
        CONTAINER_ID="$1"
        tell_dns DELETE $CONTAINER_ID $IP_ARGS
        ;;
    expose)
        collect_cidr_args "$@"
        shift $CIDR_COUNT
        get_ipam_cidr $CONTAINER_NAME weave:expose
        if [ $# -eq 0 ]; then
            FQDN=""
        else
            [ $# -eq 2 -a "$1" = "-h" ] || usage
            FQDN="$2"
        fi
        create_bridge --without-ethtool
        for CIDR in $CIDR_ARGS; do
            if ! ip addr show dev $BRIDGE | grep -qF $CIDR
            then
                ip addr add dev $BRIDGE $CIDR
                arp_update $BRIDGE $CIDR
                add_iptables_rule nat WEAVE -d $CIDR ! -s $CIDR -j MASQUERADE
                add_iptables_rule nat WEAVE -s $CIDR ! -d $CIDR -j MASQUERADE
                if [ "$FQDN" ]; then
                    http_call $DNS_CONTAINER_NAME $DNS_HTTP_PORT PUT /name/weave:expose/${CIDR%/*} --data-urlencode "fqdn=$FQDN" 2>/dev/null || true
                fi
            fi
        done
        ;;
    hide)
        collect_cidr_args "$@"
        shift $CIDR_COUNT
        get_ipam_cidr $CONTAINER_NAME weave:expose
        create_bridge --without-ethtool
        for CIDR in $CIDR_ARGS; do
            if ip addr show dev $BRIDGE | grep -qF $CIDR
            then
                ip addr del dev $BRIDGE $CIDR
                delete_iptables_rule nat WEAVE -d $CIDR ! -s $CIDR -j MASQUERADE
                delete_iptables_rule nat WEAVE -s $CIDR ! -d $CIDR -j MASQUERADE
                http_call $DNS_CONTAINER_NAME $DNS_HTTP_PORT DELETE /name/weave:expose/${CIDR%/*} 2>/dev/null || true
            fi
        done
        [ $CIDR_COUNT -eq 0 ] && http_call $CONTAINER_NAME $HTTP_PORT DELETE /ip/weave:expose 2>/dev/null || true
        ;;
    stop)
        [ $# -eq 0 ] || usage
        if ! docker stop $CONTAINER_NAME >/dev/null 2>&1 ; then
            echo "Weave is not running." >&2
        fi
        docker rm -f $CONTAINER_NAME >/dev/null 2>&1 || true
        conntrack -D -p udp --dport $PORT >/dev/null 2>&1 || true
        ;;
    stop-dns)
        [ $# -eq 0 ] || usage
        if ! docker stop $DNS_CONTAINER_NAME >/dev/null 2>&1 ; then
            echo "WeaveDNS is not running." >&2
        fi
        docker rm -f $DNS_CONTAINER_NAME >/dev/null 2>&1 || true
        ;;
    reset)
        [ $# -eq 0 ] || usage
        http_call $CONTAINER_NAME $HTTP_PORT DELETE /peer >/dev/null 2>&1 || true
        docker stop  $CONTAINER_NAME     >/dev/null 2>&1 || true
        docker stop  $DNS_CONTAINER_NAME >/dev/null 2>&1 || true
        docker rm -f $CONTAINER_NAME     >/dev/null 2>&1 || true
        docker rm -f $DNS_CONTAINER_NAME >/dev/null 2>&1 || true
        conntrack -D -p udp --dport $PORT >/dev/null 2>&1 || true
        destroy_bridge
        for LOCAL_IFNAME in $(ip link show | grep v${CONTAINER_IFNAME}pl | cut -d ' ' -f 2 | tr -d ':') ; do
            ip link del $LOCAL_IFNAME
        done
        ;;
    rmpeer)
        [ $# -eq 1 ] || usage
        PEER=$1
        http_call $CONTAINER_NAME $HTTP_PORT DELETE /peer/$PEER
        ;;
    *)
        echo "Unknown weave command '$COMMAND'" >&2
        usage
        ;;
esac
