#!/bin/bash -e
#
# tapdisk Xen block device hotplug script
#
# Author George Dunlap <george.dunlap@eu.citrix.com>
#
# Based on block-iscsi by Roger Pau Monné <roger.pau@citrix.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation; version 2.1 only. with the special
# exception on linking described in file LICENSE.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# Usage:
#
# Disks should be specified using the following syntax:
#
# For use with tapback (vbd3) (preferred):
# vdev=xvda,backendtype=tap,format=vhd,target=/srv/target.vhd
#
# For use with blkback and the blktap2 kernel module:
# script=block-tap,vdev=xvda,target=<type>:<file>
#
# format/<type> is either "aio" (for raw files), or "vhd"

dir=$(dirname "$0")
. "$dir/block-common.sh"

remove_label()
{
    echo $1 | sed "s/^\("$2"\)//"
}

check_tools()
{
    if ! command -v tap-ctl > /dev/null 2>&1; then
        fatal "Unable to find tap-ctl tool"
    fi
}

# Sets the following global variables based on the params field passed in as
# a parameter: type file
parse_target()
{
    params=($(echo "$1" | tr ":" "\n"))

    type=${params[0]}
    file=${params[1]}
    if [ -z "$type" ] || [ -z "$file" ]; then
        fatal "Cannot parse required parameters"
    fi
}

# Sets $pid and $minor to point to the device associated with the target
find_device()
{
    local info
    local param

    if [ -z "$type" ] || [ -z "$file" ]; then
        fatal "required parameters not set"
    fi

    info=$(tap-ctl list -t $type -f $file)

    for param in $(echo "$info" | tr "," "\n")
    do
        case $param in
        pid=*)
            pid=$(remove_label $param "pid=")
            ;;
        minor=*)
            minor=$(remove_label $param "minor=")
            ;;
        esac
    done

    if [ -z "$pid" ] || [ -z "$minor" ]; then
        return 1
    fi

    return 0
}

count_using()
{
    local file="$1"
    local dom
    local dev
    local f

    local i=0
    local base_path="$XENBUS_BASE_PATH/$XENBUS_TYPE"
    for dom in $(xenstore-list "$base_path")
    do
        for dev in $(xenstore-list "$base_path/$dom")
        do
            f=$(xenstore_read_default "$base_path/$dom/$dev/params" "")
            f=$(echo "$f" | cut -d ":" -f 2)

            if [ -n "$f" ] && [ "$file" = $f ] ; then
                i=$(( i + 1 ))
            fi
        done
    done

    echo "$i"
}

# tap_shared is used to determine if a shared tap can be closed
# Since a stubdom and a guest both use the same tap, it can only
# be freed when there is a single one left.
tap_shared() {
    [ $( count_using "$file" ) -gt 1 ]
}

check_tap_sharing()
{
    local file="$1"
    local mode="$2"
    local dom
    local dev

    local base_path="$XENBUS_BASE_PATH/$XENBUS_TYPE"
    for dom in $(xenstore-list "$base_path") ; do
        for dev in $(xenstore-list "$base_path/$dom") ; do
            local f=$(xenstore_read_default "$base_path/$dom/$dev/params" "")
            f=$(echo "$f" | cut -d ":" -f 2)

            if [ -n "$f" ] && [ "$file" = "$f" ] ; then
                if [ "$mode" = 'w' ] ; then
                    if ! same_vm $dom ; then
                        echo "guest $f"
                        return
                    fi
                else
                    local m=$(xenstore_read_default "$base_path/$dom/$dev/mode"
                                                    "")
                    m=$(canonicalise_mode "$m")

                    if [ "$m" = 'w' ] ; then
                        if ! same_vm $dom ; then
                            echo "guest $f"
                            return
                        fi
                    fi
                fi
            fi
        done
    done

    echo 'ok'
}

tap_create()
{
    if ! minor=$( tap-ctl allocate ) ; then
        fatal "Could not allocate minor"
    fi

    # Handle with or without kernel blktap
    minor=${minor#/run/blktap-control/tapdisk/tapdisk-}
    minor=${minor#/dev/xen/blktap-2/tapdev}

    # tap-ctl is spawning tapdisk which would hold the _lockfd open.
    # Ensure it is closed before running tap-ctl spawn, which needs to be
    # done in a subshell to continue holding the lock in the parent.
    if ! pid=$( ( eval "exec $_lockfd>&-" ; tap-ctl spawn ) ) ; then
        tap-ctl free -m "$minor"
        fatal "Could not spawn tapdisk for $minor"
    fi

    if ! tap-ctl attach -p "$pid" -m "$minor" ; then
        tap-ctl free -m "$minor"
        fatal "Could not attach $pid and $minor"
    fi

    if ! tap-ctl open -p "$pid" -m "$minor" -a "$target" ; then
        tap-ctl detach -p "$pid" -m "$minor"
        tap-ctl free -m "$minor"
        fatal "Could not open \"$target\""
    fi
}

# Attaches the device and writes xenstore backend entries to connect
# the device
add()
{
    local result

    claim_lock "block"

    if find_device; then
        result=$( check_tap_sharing "$file" "$mode" )
        if [ "$result" != "ok" ] ; then
            do_ebusy "tap $type file $file in use " "$mode" "${result%% *}"
        fi
    else
        tap_create
    fi

    xenstore_write "$XENBUS_PATH/pid" "$pid"
    xenstore_write "$XENBUS_PATH/minor" "$minor"

    if [ "$XENBUS_TYPE" = "vbd3" ] ; then
        # Create nbd unix path.  find_device/tap_create set pid & minor
        dev=$( printf "/run/blktap-control/nbd%ld.%d" "$pid" "$minor" )

        # $dev, as a unix socket, has major:minor 0:0.  If write_dev writes
        # physical-device, tapback would use that incorrect minor 0.  So don't
        # write physical-device.
        xenstore_write "$XENBUS_PATH/physical-device-path" "$dev"

        success
    else
        # Construct dev path from minor
        dev="/dev/xen/blktap-2/tapdev$minor"
        [ -b "$dev" ] || fatal "blktap \"$dev\" is not a block dev"
        write_dev "$dev"
    fi

    release_lock "block"
}

# Disconnects the device
remove()
{
    local minor
    local pid

    claim_lock "block"

    if tap_shared ; then
        return
    fi

    minor=$( xenstore_read "$XENBUS_PATH/minor" )
    pid=$( xenstore_read "$XENBUS_PATH/pid" )

    [ -n "$minor" ] || fatal "minor missing"
    [ -n "$pid" ] || fatal "pid missing"
    do_or_die tap-ctl destroy -p "$pid" -m "$minor" > /dev/null

    release_lock "block"
}

command=$1
target=$(xenstore-read $XENBUS_PATH/params || true)
if [ -z "$target" ]; then
    fatal "No information about the target"
fi

parse_target "$target"

check_tools || exit 1

mode=$( xenstore_read $XENBUS_PATH/mode )
mode=$( canonicalise_mode $mode )

# needed for same_vm
FRONTEND_ID=$(xenstore_read "$XENBUS_PATH/frontend-id")
FRONTEND_UUID=$(xenstore_read_default \
                    "/local/domain/$FRONTEND_ID/vm" 'unknown')

case $command in
add)
    add
    ;;
remove)
    remove
    ;;
*)
    exit 1
    ;;
esac
