#!/bin/bash

TCM_BIN="/usr/libexec/vstorage-iscsi/tcm/bin"
TCM_DAEMON="$TCM_BIN/pcs_tcm_utils"
TCM_ADM=/usr/bin/targetcli
TCM_LOG_FILE="/var/log/vstorage/vstorage-iscsid.log"
TCM_SYSFS="/sys/kernel/config/target"
TCM_SYSFS_CORE="/sys/kernel/config/target/core"

# systemd keep environment variables in memory, reset them on target change
TCM_NAME=""
SYSFS_NAME=""
TCM_PATH=""
TCM_EP=""

TCM_HIST="1 2 3 4 5 6 7 8 9 \
          10 20 30 40 50 60 70 80 90 \
          100 200 300 400 500 600 700 800 900 \
          1000 2000 3000 4000 5000 6000 7000 8000 9000"

function tcm_check_scsi_transport() {
	[ "${SCSI_TRANSPORT}" == "iscsi" -o "${SCSI_TRANSPORT}" == "fc" ] && return 0

	echo "tcm: unsupported scsi transport \"${SCSI_TRANSPORT}\"" 1>&2
	return 1
}

function _tcm_daemon_start() {
	$TCM_DAEMON "$SYSFS_NAME" "$TCM_EP" "-D"
	return $?
}

function _tcm_daemon_stop() {
	pid=$(ps aux | grep -e "$TCM_DAEMON\s*$SYSFS_NAME" | awk -F " " '{print $2}')
	[ -z "$pid" ] && return 0

	kill "$pid"
	return $?
}

# transform wwpn to format 00:11:22:33:44:55:66:77
function _wwpn_to_sysfs_format() {
	local wwpn="$1"

	if [ ${#wwpn} -ne 16 ] ; then
		echo "Invalid wwpn \"$wwpn\"" 1>&2
		return 1
	fi

	sysfs=$(echo "$wwpn" | sed 's/\([[:xdigit:]]\{2\}\)/\1:/g')

	if [ ${#sysfs} -ne 24 ] ; then
		echo "Invalid wwpn \"$wwpn\"" 1>&2
		return 1
	fi

	echo ${sysfs::-1}
	return 0
}

# An FC endpoint can be detected using a prefix in a symbolic name of HBA in SYSFS
# 'fcoe ...' - FCoE device
# 'QLE2...'  - QLogic devices (qla2xxx)
function tcm_set_transport() {
	local "$@"

	SCSI_TRANSPORT="$transport"
	tcm_check_scsi_transport
	[ $? -ne 0 ] && return 1

	if [ "$SCSI_TRANSPORT" == "iscsi" ] ; then
		TCM_NAME="$target"
		SYSFS_NAME="$target"
		TCM_PATH="/iscsi/$target/tpg1"
		TCM_EP="iscsi"
	elif [ "$SCSI_TRANSPORT" == "fc" ] ; then
		TCM_NAME="$(addr=`cat "$ISCSI_ROOT/$target/control/address"`; fc_validate_addr "$addr")"
		if [ $? -ne 0 -o -z "$TCM_NAME" ] ; then
			echo "Invalid wwpn in $ISCSI_ROOT/$target/control/address" 1>&2
			return 2
		fi

		SYSFS_NAME=$(_wwpn_to_sysfs_format "$TCM_NAME")
		[ $? -ne 0 ] && return 3

		hba=$(fc_list_hba | grep "$TCM_NAME" | cut -d '|' -f1)
		if [ $? -ne 0 -o -z "$hba" ] ; then
			echo "Can't find a hba with wwpn = \"$TCM_NAME\"" 1>&2
			return 4
		fi

		if [[ "$hba" == "fcoe"* ]] ; then
			TCM_EP="tcm_fc"
		elif [[ "$hba" == "QLE2"* ]] ; then
			TCM_EP="qla2xxx"
		else
			echo "Unsupported FC HBA \"$hba\"" 1>&2
			return 5
		fi

		TCM_PATH="/$TCM_EP/naa.$TCM_NAME"
	fi

	return 0
}

# ---------------------------------------------------------------------------------------
# ISCSI transport

function _tcm_add_portal_iscsi() {
	local "$@"
	$TCM_ADM "$TCM_PATH/portals" create "$ip_address" "$port" >>${TCM_LOG_FILE} 2>&1
	return $?
}

function _tcm_create_target_iscsi() {
	local "$@"

	$TCM_ADM /iscsi create "$target" >>${TCM_LOG_FILE} 2>&1
	[ $? -ne 0 ] && return 1

	tcm_disable_target
	if [ $? -ne 0 ]; then
		echo "Failed to disable target: $target" 1>&2
		return 2
	fi

	for addr in $addresses; do
		local ip_address="$(echo "$addr" | cut -d '/' -f1)"
		tcm_add_portal "target=$target" "ip_address=$ip_address" "port=$ISCSI_PORT"
		if [ $? -ne 0 ] ; then
			echo "Failed to create portal $ip_address:$ISCSI_PORT" 1>&2
			return 3
		fi
	done

	return 0
}

function _tcm_show_initiators_iscsi() {
	local "$@"
	[ -z "$portals" ] && return 0

	user_status="unauthorized"
	rc=$($TCM_ADM "$TCM_PATH" get attribute authentication | cut -d '=' -f2)
	if [ $? -eq 0 -a $rc -ne 0 ]; then
		user_status="authorized"
	fi

	ss -napt -o state established | grep "$ISCSI_PORT" | \
		grep "$portals" | \
		awk -v user_status=$user_status ' { printf user_status " %s\n", $4} '

	return $?
}

# this is a BUG in targetcli: https://bugzilla.redhat.com/show_bug.cgi?id=1161489
# so find out active sessions via config_fs
function _tcm_target_has_active_sessions_iscsi() {
	nr_sessions=$(cat $TCM_SYSFS/iscsi/$SYSFS_NAME/fabric_statistics/iscsi_instance/sessions 2>/dev/null)
	[ $? -ne 0 ] && return 0
	return "$nr_sessions"
}

# LIO/ACL uses 2 types of authorization:
# - using initiator IQN + credentials
# - using credentials
#
# WebCP assign user without IQN, so this function doesn't create IQN ACL, so generate_node_acls must be enabled
function _tcm_add_account_iscsi() {
	local "$@"
	$TCM_ADM "$TCM_PATH" set attribute authentication=1 >>${TCM_LOG_FILE} 2>&1 && \
		$TCM_ADM "$TCM_PATH" set auth "userid=$user" "password=$passw" >>${TCM_LOG_FILE} 2>&1
}

# ---------------------------------------------------------------------------------------
# FC/FCoE transport

function _tcm_add_portal_fc() {
	echo "'${FUNCNAME[0]}()' is not supported yet for SCSI_TRANSPORT=fc" >>${TCM_LOG_FILE}
	exit 1
}

function _tcm_create_target_fc() {
	$TCM_ADM "/$TCM_EP" create "$TCM_NAME" >>${TCM_LOG_FILE} 2>&1
	[ $? -ne 0 ] && return 1

	return 0
}

function _tcm_show_initiators_fc() {
	local user="unauthorized"

	initiators=$(fc_get_initiators "$TCM_NAME")
	[ -z "$initiators" ] && return 0

	account="$ISCSI_ROOT/$target/control/account"
	if [ -f $account ] ; then
		user=$(cat $account)
	fi

	for i in $initiators; do
		$TCM_ADM "/$TCM_PATH/acls/naa.$i" >/dev/null 2>&1
		if [ $? -ne 0 ]; then
			echo "unauthorized $i"
		else
			echo "$user $i"
		fi
	done

	return 0
}

# NOTE: A FCoE target doesn't export any information in a sys_fs in vn2vn mode (in current kernel 3.10)
function _tcm_target_has_active_sessions_fc() {
	initiators=$(fc_get_initiators "$TCM_NAME" 2>/dev/null)
	[ $? -ne 0 ] && return 0

	for i in $initiators; do
		$TCM_ADM "$TCM_PATH/acls/$i" >/dev/null 2>&1
		if [ $? -eq 0 ]; then
			return 1
		fi
	done

	return 0
}

# In current version (a may be always) TCM FCoE doesn't support authentication via user/password info.
# Also TCM FCoE doesn't allow dynamic ACL, so let's use account for zonning in following way:
# <username> - symbolic name which can be used in `show_initiator`
# <password> - WWPN of neighbour
#
# NOTE: by default TCM mappes all new LUNs to all acl's, so doesn't matter which LUNs exist now
function _tcm_add_account_fc() {
	local "$@"
	$TCM_ADM "$TCM_PATH/acls" create "$passw" >>${TCM_LOG_FILE} 2>&1
	return $?
}

# ---------------------------------------------------------------------------------------

function tcm_add_portal() {
	local "$@"
	_tcm_add_portal_${SCSI_TRANSPORT} "target=$target" "ip_address=$ip_address" "port=$ISCSI_PORT"
	return $?
}

function _tcm_verify_target_existence() {
	msg=$($TCM_ADM "$TCM_PATH" 2>&1)
	[ $? -eq 0 ] && return 0

	if [[ "$msg" == "No such path"* ]] ; then
		return 2
	fi

	return 3
}

function tcm_create_target() {
	local "$@"

	# TODO: configure this params in the config file and remove the command bellow
	# target discovery is available without CHAP auth, like in tgtd
	$TCM_ADM set global auto_add_default_portal=false auto_enable_tpgt=false >>${TCM_LOG_FILE} 2>&1 && \
		$TCM_ADM /iscsi set discovery_auth enable=0 >>${TCM_LOG_FILE} 2>&1
	rc=$?
	if [ $rc -ne 0 ]; then
		echo "Failed configure TCM" 1>&2
		return $rc
	fi

	_tcm_create_target_${SCSI_TRANSPORT} target=$target addresses="$addresses"

	# Persistent reservation is enabled from the box (???):
	# http://linux-iscsi.org/wiki/Persistent_Reservations
	# path: /var/target/pr/*

	return $?
}

function _tcm_list_luns() {
	ls "$TCM_SYSFS/$TCM_EP/$SYSFS_NAME/tpgt_1/lun/" 2>/dev/null | cut -d '_' -f2
}

function _tcm_enable_hist() {
	local lun="$1"

	echo "${TCM_HIST}" | tee $TCM_SYSFS/$TCM_EP/$SYSFS_NAME/tpgt_1/lun/lun_${lun}/statistics/scsi_tgt_port/write_hist >/dev/null 2>&1 && \
		echo "${TCM_HIST}" | tee $TCM_SYSFS/$TCM_EP/$SYSFS_NAME/tpgt_1/lun/lun_${lun}/statistics/scsi_tgt_port/read_hist >/dev/null 2>&1 && \
		echo "${TCM_HIST}" | tee $TCM_SYSFS/$TCM_EP/$SYSFS_NAME/tpgt_1/lun/lun_${lun}/statistics/scsi_tgt_port/sync_hist >/dev/null 2>&1

	if [ $? -ne 0 ] ; then
		echo "$TCM_NAME: can't configure histograms for LUN=$lun" >>${TCM_LOG_FILE}
		return 1
	fi

	return 0
}

function tcm_delete_target() {
	local "$@"

	# this checking prevents errors after incomplete target deletion
	_tcm_verify_target_existence
	[ $? -ne 0 ] && return 0

	portals=$(tcm_show_target_portals) 2>>${TCM_LOG_FILE}
	if [ -z "$portals" ]; then
		echo "Can't find portals for target '$target'" 1>&2
		return 1
	fi

	if [ -z "$force" ]; then
		busy=1
		attempts=10
		while true; do
			_tcm_target_has_active_sessions_${SCSI_TRANSPORT}
			busy=$?
			attempts=$((attempts-1))
			if [ "$attempts" -eq 0 -o "$busy" -eq 0 ] ; then
				break
			fi
			sleep 2
		done

		if [ "$busy" -ne 0 ] ; then
			echo "Can't stop TCM target, there are active initiators" 1>&2
			return 3
		fi
	fi

	# detach all luns directrly to prevent ploop holding
	luns=$(_tcm_list_luns)
	for lun_id in $luns; do
		tcm_detach_lun "lun_id=$lun_id" "target=$target"
	done

	$TCM_ADM /$TCM_EP delete "$TCM_NAME" >>${TCM_LOG_FILE} 1>&2
	[ $? -ne 0 ] && return 4

	return 0
}

# authentication params:
#
# authentication=0
# ----------------
# If set to 1, enforce authentication for this TPG.
#
# generate_node_acls=0
# --------------------
# If set to 1, allow all initiators to login (i.e. demo mode).
#
# demo_mode_write_protect=0
# -------------------------
# If set to 1 in demo mode, prevent writes to LUNs
function tcm_disable_authentication() {
	local "$@"

	case $SCSI_TRANSPORT in
		"iscsi")
			$TCM_ADM $TCM_PATH set attribute authentication=0 generate_node_acls=1 demo_mode_write_protect=0 >>${TCM_LOG_FILE} 2>&1
			;;
		"fc")
			# doesn't make sense for vn2vn + fc doesn't support dynamic acls, only WWPN authentication
			echo "">/dev/null
			;;
	esac

	return $?
}

function tcm_add_account() {
	local "$@"
	_tcm_add_account_${SCSI_TRANSPORT} "target=$target" "user=$user" "passw=$passw"
	return $?
}

function tcm_configure_target() {
	local "$@"
	# TODO: use targetcli (`get attribute`, `get parameter`, info, etc) to see all settings
	return 0
}

function tcm_enable_target() {
	$TCM_ADM "$TCM_PATH" enable >>${TCM_LOG_FILE} 2>&1
	return $?
}

function tcm_disable_target() {
	$TCM_ADM "$TCM_PATH" disable >>${TCM_LOG_FILE} 2>&1
	return $?
}

function tcm_get_target_status() {
	status=$($TCM_ADM "$TCM_PATH" info 2>/dev/null)
	if [ $? -ne 0 ]; then
		echo "service failed"
		return 1
	fi

	echo "$status" | grep "enable:\s*True">/dev/null
	if [ $? -ne 0 ]; then
		echo "not running"
		return 2
	fi

	echo "running"
	return 0
}

function tcm_show_target_portals() {
	case $SCSI_TRANSPORT in
		"iscsi")
			ls -1 "$TCM_SYSFS/iscsi/$SYSFS_NAME/tpgt_1/np/" 2>/dev/null
			;;
		"fc")
			echo "$TCM_NAME"
			;;
	esac
}

# Research info:
# 1) Linux kernel since 4.1 provides target_show_dynamic_sessions() call.
# It's used to see dynamic ACLs in config_fs
#
# 2) There is no stable way to get dynamic ALC in CentOS kernel (3.10.*)
function tcm_show_initiators() {
	portals=$(tcm_show_target_portals)
	_tcm_show_initiators_${SCSI_TRANSPORT} portals="$portals"
	return $?
}

# ---------------------------------------------------------------------------------------
# Target port group functions

# User helper callback is called before target port group state changed.
# Callback is executed with the following parameters:
# TG_PT_Group ID prev_state new_state {explicit/implicit} dev_name
# For exmaple:
# default_tg_pt_gp 0 Active/Optimized Standby explicit iqn.2014-06.com.vstorage:test-2
#
function _tcm_add_user_helper() {
    local storage_object="$1"
    local tpg="$2"
    local cb="$3"
    local user_helper="$TCM_SYSFS_CORE/*/${storage_object}/alua/${tpg}/user_helper"

    if [ ! -f $user_helper ]; then
        echo "user_helper doesn't supported by tpg ($user_helper)" 1>&2
        return 1
    fi

    if [ -n "$cb" -a "$cb" != "-" -a -f $cb ]; then
        echo "Target port group callback script doesn't exists ($cb)" 1>&2
        return 1
    fi

    echo -n $cb > $user_helper
    if [ $? -ne 0 ]; then
        echo "Failed to set tpg callback into user helper ($cb > $user_helper)" 1>&2
        return 1
    fi

    return 0
}

function tcm_create_tpg() {
	local storage_object="$1"
	local tpg="$2"
	local tag="$3"

	$TCM_ADM ${storage_object}/alua create name=${tpg} tag=${tag} >>${TCM_LOG_FILE} 2>&1
	if [ $? -ne 0 ]; then
		echo "Failed to create target port group \"$storage_object ($tpg $tag)\"" 1>&2
		return 1
	fi

	return 0
}

# Target port group access state:
# 0 - Active/optimized
# 1 - Active/non-optimized
# 2 - Standby
# 3 - Unavailable
function _tcm_set_alua_access_state() {
	local storage_object="$1"
	local tpg="$2"
	local state="$3"
	
	$TCM_ADM ${storage_object}/alua/${tpg} set alua alua_access_state=$state >>${TCM_LOG_FILE} 2>&1
	if [ $? -ne 0 ]; then
		echo "Failed to set target port group access state for \"${storage_object}/alua/${tpg} alua_access_state=$state\"" 1>&2
		return 1
	fi

	return 0
}

function tcm_set_tpg_ao() {
	_tcm_set_alua_access_state $1 $2 0
	return $?
}
function tcm_set_tpg_ano() {
	_tcm_set_alua_access_state $1 $2 1
	return $?
}
function tcm_set_tpg_stby() {
	_tcm_set_alua_access_state $1 $2 2
	return $?
}
function tcm_set_tpg_unavailable() {
	_tcm_set_alua_access_state $1 $2 3
	return $?
}

# ---------------------------------------------------------------------------------------
# Luns functions

function tcm_set_lun_tpg() {
	local lun_id="$1"
	local tpg="$2"

	$TCM_ADM $TCM_PATH/luns/lun$lun_id set alua alua_tg_pt_gp_name=${tpg} >>${TCM_LOG_FILE} 2>&1
	if [ $? -ne 0 ]; then
		echo "Failed to set lun target port group \"$TCM_PATH/luns/lun$lun_id ($tpg)\"" 1>&2
		return 1
	fi

	return 0
}

function tcm_attach_lun() {
	local "$@"

	backstore_name=${target}-${lun_id}
	local backstore_type=fileio
	[ -b $dev ] && backstore_type=block

	if [ -b "$dev" ]; then
		$TCM_ADM /backstores/block create "name=${backstore_name}" "dev=${dev}" "wwn=${serial_sn}" >>${TCM_LOG_FILE} 2>&1
	else
		$TCM_ADM /backstores/fileio create "name=${backstore_name}" "file_or_dev=${dev}" size=0 "wwn=${serial_sn}" >>${TCM_LOG_FILE} 2>&1
	fi

	if [ $? -ne 0 ] ; then
		echo "Failed to create storage object \"$backstore_name ($dev)\"" 1>&2
		return 1
	fi

	ret=0;
	$TCM_ADM "/backstores/$backstore_type/${backstore_name}" set attribute unmap_zeroes_data=1 emulate_tpu=1 emulate_tpws=1 >>${TCM_LOG_FILE} 2>&1
	if [ $? -ne 0 ]; then
		echo "Failed to set attributes for $target/$lun_id" 1>&2
		ret=1
	fi
	$TCM_ADM $TCM_PATH/luns create "/backstores/$backstore_type/${backstore_name}" "$lun_id" >>${TCM_LOG_FILE} 2>&1
	if [ $? -ne 0 ] ; then
		echo "Failed to create LUN $lun_id for target $target)" 1>&2
		ret=1;
	fi
	if [ "$ret" -ne 0 ]; then
		$TCM_ADM "/backstores/$backstore_type" delete "${backstore_name}" >>${TCM_LOG_FILE} 2>&1
		return 2
	fi

	_tcm_enable_hist "$lun_id"
	# ignore an error, kernel can be unpatched

	return 0
}

function tcm_detach_lun() {
	local "$@"

	backstore_name=${target}-${lun_id}

	$TCM_ADM "$TCM_PATH/luns/" delete "lun${lun_id}" >>${TCM_LOG_FILE} 2>&1
	[ $? -ne 0 ] && return 1

	msg=$($TCM_ADM /backstores/fileio delete "$backstore_name" 2>&1)
	if [ $? -eq 0 ] ; then
		echo "$target: $msg" >>${TCM_LOG_FILE}
		return 0
	fi

	msg=$($TCM_ADM /backstores/block delete "$backstore_name" 2>&1)
	if [ $? -eq 0 ] ; then
		echo "$target: $msg" >>${TCM_LOG_FILE}
		return 0
	fi

	echo "$target: $msg" >>${TCM_LOG_FILE}
	echo "Can't detach storage object \"$backstore_name\"" 1>&2
	return 3
}

function tcm_is_lun_online() {
	local "$@"

	$TCM_ADM "$TCM_PATH/luns/lun$lun_id" 1>/dev/null 2>&1
	if [ $? -ne 0 ]; then
		echo "No"
		return
	fi

	$TCM_ADM "$TCM_PATH" info | grep "enable: True" 1>/dev/null 2>&1
	if [ $? -ne 0 ]; then
		echo "No"
		return
	fi

	echo "Yes"
}

function tcm_detach_dev() {
	local "$@"
	local backstore_name="$target-$lun_id"
	local backstore_type=fileio
	[ -b "$dev" ] && backstore_type=block

	msg=$($TCM_ADM "/backstores/$backstore_type" delete "$backstore_name" 2>&1)
	rc=$?

	[ "$rc" -eq 0 ] && return 0

	[ "$msg" == "No storage object named $backstore_name." ] && return 0

	echo "Can't detach storage object \"$backstore_name ($dev)\"" 1>&2
	echo "$msg"

	return 1
}

function tcm_show_lun_stats() {
	local "$@"
	echo ""
}

function tcm_set_limit() {
	echo "LIO/TCM set_limit isn't supported" 1>&2
	return 1
}

function tcm_rescan_lun() {
	# this is not required for TCM, all changes are detected by a SCSI system
	return 0
}
