#!/bin/bash # # wire-network-container.sh # MIT License # # attaches multiple containers to multiple ovs bridges # exit codes: # 0 action completed successfully # 1 unknown option/action or no action given # 2 No device/bridge part specified. # 3 No container ids specified. # 10 No running containers found. # 100 action failed

# Display usage and help function usage() {

echo 'attach-container.sh <COMMAND> [OPTIONS] -- <device:bridge> [<device:bridge>] <container-id> [<container-id>]'
echo '  where COMMAND is one of'
echo '    verify        verify given container/device/bridge options'
echo '    attach        attach devices to bridges and containers'
echo '    detach        detach devices from bridges and containers'
    echo '  where OPTIONS are'
    echo '    -n/--noop         only print commands, do not change'
    echo '    -d/--debug        show debug output'
    echo '    -q/--quiet        be quiet'
    echo '    -s/--state <FILE>   write state to <FILE>'
    echo '  Examples:'
    echo '   attach-containers.sh attach --debug -- eth1:br1:NODHCP eth2:br2 02978729 12673482'
    echo '   attach-containers.sh verify -- eth1:br1 eth2:br2 02978729 12673482'
    echo '   attach-containers.sh detach -q -- eth1:br1 eth2:br2 02978729 12673482'

}

# – FUNCTION —————————————————————- # Name: split_args # Description: Splits up args after – and puts them in DEVICE_ARR/ID_ARR arrays # —————————————————————————- function split_args() {

for ARG in $@; do
        if [[ "$ARG" =~ .*:.* ]] ; then
                DEVICE_ARR="$DEVICE_ARR $ARG"
        else
                ID_ARR="$ID_ARR $ARG"
        fi
done

}

# DEBUG and dummy output function nodebug() {

echo $* >/dev/null

} function debug() {

[[ "$QUIET" != "1" ]] && echo DEBUG $*

} function log_error() {

[[ "$QUIET" != "1" ]] && echo ERROR $* >&2

} function log_ok() {

[[ "$QUIET" != "1" ]] && echo OK $*

}

NOOP= MODE= ACTION= DEBUG=nodebug QUIET= STATEFILE=

while : do

case "$1" in
  verify | attach | detach)
      ACTION=$1
      shift
      ;;
  -h | --help)
      usage
      exit 0
      ;;
  -n | --noop)
      NOOP=1
          shift
      ;;
  -s | --state)
          shift
          STATEFILE=$1
          shift
      ;;
  -d | --debug)
      DEBUG=debug
          shift
      ;;
  -q | --quiet)
      QUIET=1
          shift
      ;;
  --) # End of all options
      shift
          break
      ;;
  -*)
      echo "Error: Unknown option: $1" >&2
      exit 1
      ;;
  *)  # No more options
      break
      ;;
esac

done

# Check mandatory input arguments if [[ -z “$ACTION” ]]; then

log_error No action given, see usage
usage
exit 1

fi

split_args $@

if [[ -z “$DEVICE_ARR” ]]; then

log_error No device/bridge part specified.
usage
exit 2

else

$DEBUG Attaching devices/bridges "$DEVICE_ARR"

fi if [[ -z “$ID_ARR” ]]; then

log_error No container ids specified.
usage
exit 3

fi

# empty state file if [[ ! -z “$STATEFILE” ]]; then

echo "# This is a wire network state file" >"$STATEFILE"
echo "TIMESTAMP=`date`" >>"$STATEFILE"

fi

[ “$NOOP” -eq 1 ]

&& MODE=‘echo NOOP% ’

# DEBUG PS4=‘+|${BASH_SOURCE##*/} ${LINENO}${FUNCNAME:+ ${FUNCNAME}}| ’

# locate binaries # TODO: add defaults IP=$(which ip) OVS_VSCTL=$(which ovs-vsctl) DOCKER=$(which docker) DHCLIENT=$(which dhclient)

# – FUNCTION —————————————————————- # Name: add_to_state # Description: Adds given text line to state file. If STATEFILE is empty, # nothing is done # Parameters # 1: state info line # —————————————————————————- function add_to_state() {

if [[ ! -z "$STATEFILE" ]]; then
    echo $* >>"$STATEFILE"
    fi

}

# – FUNCTION —————————————————————- # Name: container_process # Description: Given ID of container, this returns the Process id # Parameters # 1: Docker Container ID # Returns : Container Process ID # —————————————————————————- function container_process() {

T="$1"
local PID=$(sudo docker inspect -f '{{ .State.Pid }}' "$T")

echo $PID

}

# – FUNCTION —————————————————————- # Name: link_netns # Description: Puts link in /var/run/netns according to given process id # Parameters # 1: Container Process ID # —————————————————————————- function link_netns() {

$MODE sudo mkdir -p /var/run/netns
local PID="$1"
$MODE sudo ln -s /proc/$PID/ns/net /var/run/netns/$PID

}

# – FUNCTION —————————————————————- # Name: unlink_netns # Description: removes link from /var/run/netns # Parameters # 1: Container Process ID # —————————————————————————- function unlink_netns() {

    local PID=$1
    if [[ -z $PID ]]; then
        log_error unlink_netns: pid not specified
    else
        $MODE sudo rm "/var/run/netns/$PID"
fi

}

# – FUNCTION —————————————————————- # Name: get_mtu # Description: Retrieves the MTU for a given device # Parameters # 1: Device # Returns : MTU # —————————————————————————- function get_mtu() {

local DEV=$1
local MTU=$(ip link show $DEV | grep mtu | sed -e 's/.*mtu \([0-9]*\) .*/\1/')

echo $MTU

}

# – FUNCTION —————————————————————- # Name: add_peer_interfaces # Description: creates the host/container peer interfaces # Parameters # 1: Host interface name # 2: Container Interface name # 3: mtu on bridge # —————————————————————————- function add_peer_interfaces() {

local HOST_IF=$1
local CONTAINER_IF=$2
local MTU=$3
$MODE sudo $IP link add name $HOST_IF mtu $MTU type veth peer name $CONTAINER_IF mtu $MTU       
return $?

}

# – FUNCTION —————————————————————- # Name: add_device_to_switch # Description: Adds an interface device to given ovs bridge # Parameters # 1: interface name # 2: ovs bridge name # —————————————————————————- function add_device_to_switch() {

local HOST_IF=$1
local SWITCH=$2
$MODE sudo $OVS_VSCTL add-port $SWITCH $HOST_IF
return $?

}

# – FUNCTION —————————————————————- # Name: remove_device_from_switch # Description: Removes an interface device from given ovs bridge # Parameters # 1: interface name # 2: ovs bridge name # —————————————————————————- function remove_device_from_switch() {

local HOST_IF=$1
local SWITCH=$2
$MODE sudo $OVS_VSCTL del-port $SWITCH $HOST_IF
return $?

}

# – FUNCTION —————————————————————- # Name: configure_interfaces # Description: brings container interfaces up, sets namespace and names # see: docs.docker.com/articles/networking/#how-docker-networks-a-container # Parameters # 1: Host interface name # 2: Container Interface name (peer) # 3: Namespace (=container pid) # 4: container device name # —————————————————————————- function configure_interfaces() {

local HOST_IF=$1
local CONTAINER_IF=$2
local NS=$3
local DEVICE=$4
$MODE sudo $IP link set $HOST_IF up &&  \
$MODE sudo $IP link set $CONTAINER_IF netns $NS && \
$MODE sudo $IP netns exec $NS ip link set $CONTAINER_IF name $DEVICE
return $?

}

# – FUNCTION —————————————————————- # Name: delete_interfaces # Description: deletes the interface pair on host # Parameters # 1: Host interface name # 2: Container Interface name (peer) # 3: Namespace (=container pid) # 4: container device name # Returns : exit code of ip link delete command # —————————————————————————- function delete_interfaces() {

local HOST_IF=$1
local CONTAINER_IF=$2
local NS=$3
local DEVICE=$4
$MODE sudo $IP link delete $HOST_IF type veth peer name $CONTAINER_IF
return $?

}

# – FUNCTION —————————————————————- # Name: has_interfaces # Description: checks if container/host is set up correctly # Parameters # 1: Host interface name # 2: Container Interface name (peer) # 3: Namespace (=container pid) # 4: container device name # Returns : 0=ok, 1=failed # —————————————————————————- function has_interfaces() {

local HOST_IF=$1
local CONTAINER_IF=$2
local NS=$3
local DEVICE=$4

# container device
$MODE sudo $IP netns exec $NS $IP link show $DEVICE >/dev/null 2>&1 
if [[ $? -ne 0 ]]; then
        return 1
fi
# check if we have an ip (todo: fix in NODHCP mode)
#$MODE sudo $IP netns exec $NS $IP addr show $DEVICE 2>&1 | grep 'inet ' >/dev/null 2>&1
#if [[ $? -ne 0 ]]; then
#       return 1
#fi

# host devices
$MODE sudo $IP link show $HOST_IF >/dev/null 2>&1 
if [[ $? -ne 0 ]]; then
        return 1
fi

# TODO: CHeck container interface

return 0

}

# – FUNCTION —————————————————————- # Name: dhcp_container # Description: calls dhclient for interface of namespace # Parameters # 1: Namespace (=container pid) # 2: container device name # Returns : 0=ok, 1=failed # —————————————————————————- function dhcp_container() {

local NS=$1
local DEVICE=$2
$MODE sudo "$IP" netns exec "$NS" "$DHCLIENT" -v -1 "$DEVICE"
return $?

}

# == AGGREGATE FUNCTIONS =====================================================

# – FUNCTION —————————————————————- # Name: handle_verify # Checks if # - container ids are valid # - pids can be queried # - bridges exist and are up # - container device names not yet in use # Returns : 0=ok, 1=failed # —————————————————————————- function handle_verify() {

local RES=0

# ensure there are containers running before continuing
CURRENT_IDS=$(sudo "$DOCKER" ps -q --no-trunc)
if [[ -z "$CURRENT_IDS" ]]; then
        log_error No running containers found.
        exit 10
fi

for DEVICE_PAIR in $DEVICE_ARR; do
        BRIDGE=$(echo "$DEVICE_PAIR" | awk -F':' '{ print $2 }' )
        $DEBUG Checking "$BRIDGE"
        sudo "$OVS_VSCTL" br-exists "$BRIDGE"
        if [[ $? -eq 0 ]]; then
                log_ok "$BRIDGE"
        else
                log_error Unable to find ovs bridge "$BRIDGE"
                RES=1
        fi
done

# iterate given container ids
for ID in $ID_ARR; do
        $DEBUG Checking $ID
        if [[ ! $CURRENT_IDS =~ $ID ]]; then
                log_error No container for $ID found, skipping...
                RES=1
        else
                # get pid
                PID=$(container_process $ID)
                if [[ -z $PID ]]; then
                        log_error Unable to grab PID for $ID, skipping
                        RES=1
                else
                        log_ok $ID

                        # with pid, check given devices
                        # on host and in container

                        $DEBUG - Checking devices in $ID
                link_netns "${PID}"
        add_to_state ${ID}.CONTAINER_PID=${TARGET_PID}

                        # iterate given devices
                        for DEVICE_PAIR in $DEVICE_ARR; do
                                INTF=$(echo "$DEVICE_PAIR" | awk -F':' '{ print $1 }' )
                                $DEBUG -- Checking $INTF
                                HOST_IFNAME=v${INTF}h${PID}
                                CONTAINER_IFNAME=v${INTF}c${PID}

                                has_interfaces $HOST_IFNAME $CONTAINER_IFNAME $PID $INTF 
                                if [[ $? -eq 0 ]]; then
                                        log_ok "$ID"/"$PID" has a "$CONTAINER_IFNAME", host has a "$HOST_IFNAME"
                                else 
                                        log_error "$ID"/"$PID" does not have correct devices
                                        RES=1
                                fi
                        done

                        unlink_netns "${PID}"
                fi
        fi
done

return $RES

}

# – FUNCTION —————————————————————- # Name: handle_attach # Description: attaches all containers (of $ID_ARR) to all bridges ($DEVICE_ADDR) # with its local interface names. Calls dhclient for all interfaces. # Returns : 0=ok, 1=failed # —————————————————————————- function handle_attach() {

local RES=0

# iterate given container ids
for TARGET in $ID_ARR; do
        $DEBUG Attaching $TARGET

        TARGET_PID=$(container_process $TARGET)
$DEBUG PID of $TARGET is $TARGET_PID

link_netns "${TARGET_PID}"
add_to_state ${TARGET}.CONTAINER_PID=${TARGET_PID}

        # iterate given devices
        for DEVICE_PAIR in $DEVICE_ARR; do
                INTF=$(echo $DEVICE_PAIR | awk -F':' '{ print $1 }' )
                BRIDGE=$(echo $DEVICE_PAIR | awk -F':' '{ print $2 }' )
                FLAGS=$(echo $DEVICE_PAIR | awk -F':' '{ print $3 }' )
                $DEBUG Attaching $INTF to $BRIDGE

                BRIDGEDEV_MTU=$(get_mtu $BRIDGE)
                if [[ -z "$BRIDGEDEV_MTU" ]]; then
                        log_error querying mtu of $BRIDGE, aborting
                        RES=1
                        break   
                fi
                add_to_state ${TARGET}.BRIDGEDEV_MTU=${BRIDGEDEV_MTU}

    HOST_IFNAME=v${INTF}h${TARGET_PID}
    CONTAINER_IFNAME=v${INTF}c${TARGET_PID}
    $DEBUG - interface pair names are ${HOST_IFNAME}/${CONTAINER_IFNAME}

                $DEBUG - creating peer interfaces
        add_peer_interfaces "$HOST_IFNAME" "$CONTAINER_IFNAME" "$BRIDGEDEV_MTU"
                if [[ $? -ne 0 ]]; then 
                        log_error creating peer interfaces. aborting
                        RES=1
                        continue        
                fi

    add_to_state ${TARGET}.HOST_IFNAME=${HOST_IFNAME}
    add_to_state ${TARGET}.CONTAINER_IFNAME=${CONTAINER_IFNAME}

                $DEBUG - adding $HOST_IFNAME to $BRIDGE
        add_device_to_switch "$HOST_IFNAME" "$BRIDGE"
                if [[ $? -ne 0 ]]; then 
                        log_error adding device to bridge. aborting
                        RES=1
                        continue        
                fi
    add_to_state ${TARGET}.ON_BRIDGE=$BRIDGE

                $DEBUG - configuring interfaces
        configure_interfaces "$HOST_IFNAME" "$CONTAINER_IFNAME" "$TARGET_PID" "$INTF"
                if [[ $? -ne 0 ]]; then 
                        log_error configuring interfaces. aborting
                        RES=1
        add_to_state ${CONTAINER_IFNAME}.configured=error
                        continue
                fi
    add_to_state ${TARGET}.${CONTAINER_IFNAME}.configured=ok

        if [[ "$FLAGS" =~ NODHCP ]]; then
                    $DEBUG - skipping dhcp for $INTF
                else
                    $DEBUG - dhcp requesting address
            dhcp_container "$TARGET_PID" "$INTF"
                    if [[ $? -ne 0 ]]; then
                            log_error running dhcp. aborting
            add_to_state ${CONTAINER_IFNAME}.dhcp=error
                            RES=1
                            continue
                    fi
                    add_to_state ${TARGET}.${CONTAINER_IFNAME}.dhcp=ok

        fi

done
unlink_netns "$TARGET_PID"
done

return $RES

}

# – FUNCTION —————————————————————- # Name: handle_detach # Description: detaches all containers (of $ID_ARR) from all bridges ($DEVICE_ADDR) # Removes eth/veth pairs from host # Returns : 0=ok, 1=failed # —————————————————————————- function handle_detach() {

local RES=0

# iterate given container ids
for TARGET in $ID_ARR; do
        $DEBUG Detaching $TARGET

        TARGET_PID=$(container_process $TARGET)
$DEBUG PID of $TARGET is $TARGET_PID

link_netns ${TARGET_PID}

        # iterate given devices
        for DEVICE_PAIR in $DEVICE_ARR; do
                INTF=$(echo $DEVICE_PAIR | awk -F':' '{ print $1 }' )
                BRIDGE=$(echo $DEVICE_PAIR | awk -F':' '{ print $2 }' )

    HOST_IFNAME=v${INTF}h${TARGET_PID}
    CONTAINER_IFNAME=v${INTF}c${TARGET_PID}
    $DEBUG - interface pair names are ${HOST_IFNAME}/${CONTAINER_IFNAME}

                $DEBUG - removing $HOST_IFNAME from $BRIDGE
        remove_device_from_switch "$HOST_IFNAME" "$BRIDGE"
                if [[ $? -ne 0 ]]; then 
                        log_error removing device from bridge. aborting
                        RES=1
                        break
                fi

                $DEBUG - delete interfaces 
        delete_interfaces "$HOST_IFNAME" "$CONTAINER_IFNAME" "$TARGET_PID" "$INTF"
                if [[ $? -ne 0 ]]; then 
                        log_error deleting interfaces
                        RES=1
                        break
                fi
        done

        unlink_netns "$TARGET_PID"
done

return $RES

}

# ========= MAIN ========================================================

if [[ “$ACTION” == “verify” ]]; then

handle_verify
if [[ $? -eq 0 ]]; then 
        log_ok
        exit 0
else    
        echo FAILED
        exit 100
fi

fi

if [[ “$ACTION” == “attach” ]]; then

handle_attach
if [[ $? -eq 0 ]]; then 
        log_ok
        exit 0
else    
        echo FAILED
        exit 100
fi

fi

if [[ “$ACTION” == “detach” ]]; then

handle_detach
if [[ $? -eq 0 ]]; then 
        log_ok
        exit 0
else    
        echo FAILED
        exit 100
fi

fi