#!/usr/bin/bash
#
# Copyright (c) 2006, 2024, Oracle and/or its affiliates.
#
# oracleasm-Xshlib - Common shell functions
#


#
# Some basic functions
#


error()
{
    if [ $# -gt 0 ]
    then
        echo >&2 "$@"
    fi
}

die()
{
    error "$@"
    exit 1
}

usage()
{
    die "Usage: $(basename $0) $USAGE"
}


version()
{
    die "$0 version 3.0.0"
}


#
# Check if the permissions on the disk is OK.
# Returns 0 on success, 1 on error.
#
check_perm()
{
    if [ "$#" -lt "1" -o -z "$1" ]
    then
        die "check_perm(): Requires an argument"
    fi

    DISKNAME="$1"

    OUTPUT=$(ls -Hld "${DISKNAME}" 2>/dev/null)
    if [ $? != 0 ]
    then
        return 1
    fi

    OUID=$(echo "$OUTPUT" | awk '{print $3}')
    OGID=$(echo "$OUTPUT" | awk '{print $4}')

    if [ "$OUID" = "${ORACLEASM_UID:-root}" -a "$OGID" = "${ORACLEASM_GID:-root}" ]
    then
	# permissions are correct
	return 0
    fi

    # permissions are not correct
    return 1
}

#
# perm_disk()
# Set the appropriate permissions on all the disk paths for this disk.
#
# Returns 0 on success, 1 on error, 2 on already correct
#
perm_disk()
{
    if [ "$#" -lt "1" -o -z "$1" ]
    then
        die "perm_disk(): Requires an argument"
    fi

    DISKNAME="$1"

    case "$1" in
        /dev/*)
	    disk_paths=$1
	    ;;
        *)
            disk_paths=$(list_disk_paths "${ORACLE_ASMMANAGER}" "$1" 2>&3)
	    ;;
    esac

    for disk_path in $(echo $disk_paths)
    do
        OUTPUT=$(ls -Hld "$disk_path" 2>/dev/null)
        if [ $? != 0 ]
        then
            return 1
        fi

        OUID=$(echo "$OUTPUT" | awk '{print $3}')
        OGID=$(echo "$OUTPUT" | awk '{print $4}')

        if [ "$OUID" = "${ORACLEASM_UID:-root}" -a "$OGID" = "${ORACLEASM_GID:-root}" ]
        then
	    continue
        fi

        chown "${ORACLEASM_UID:-root}:${ORACLEASM_GID:-root}" "$disk_path" 2>&3
        if [ $? != 0 ]
        then
            return 1
        fi

        chmod 0660 "$disk_path" 2>&3
        if [ $? != 0 ]
        then
            return 1
        fi
    done

    return 0
}

#
# Make a disk name uppercase, because ASM disk labels are all uppercase
# (SQL identifiers)
#
upper_disk()
{
    case "$1" in
    *[^_a-zA-Z0-9]*)
        echo "Disk label \"$1\" contains an invalid character" >&2
        ;;
    *)
        echo "$1" | tr '[a-z]' '[A-Z]'
        ;;
    esac
}


asm_disk_path()
{
    if [ $# -lt 1 ]
    then
        die "asm_disk_path requires an argument"
    fi

    manager="$1"
    name="$2"

    if [ "$ORACLEASM_DRIVER_SUPPORTED" = "true" ]
    then
        echo "${manager}/disks/$name"
	return
    fi

    # Get the ASM disk path(s) from lsblk or blkid command
    if [ "${name}" = "" ]
    then
	# no disk specified so list all the ASM disks
        type=$(basename $manager)
        lsblk -o fstype,label|grep ${type}|sort -u|awk '{if ($2) print $2}'
    else
	# get the disk path for the specifid ASM disk (label)
	# which may be a multipath disk
	dev=$(basename $name)
	blkid -L ${dev}
    fi
}

list_disk_paths()
{
    if [ $# -lt 2 ]
    then
        die "list_disk_paths requires arguments"
    fi

    manager="$1"
    name="$2"

    if [ "$ORACLEASM_DRIVER_SUPPORTED" = "true" ]
    then
        echo "${manager}/disks/$name"
	return
    fi

    # get the disk paths for the specifid ASM disk (label)
    # which may be a multipath disk
    blkid -t LABEL=$name | awk -F '[:]' '{print $1}'
}

# Why is this fake?  Because parsing ls(1) output is fragile
_fake_stat()
{
    if [ "$#" -lt "1" -o -z "$1" ]
    then
        die "fake_stat(): Requires an argument"
    fi

    DISKPATH="$1"
    ls -l "$DISKPATH" 2>/dev/null |
        awk '$5 ~ /[0-9]+,$/{
                 sub(/,$/, "", $5);
                 printf "0x%x 0x%x\n", $5, $6;
             }'
}


get_dev()
{
    if [ "$#" -lt "1" -o -z "$1" ]
    then
        die "get_dev(): Requires an argument"
    fi

    DISKPATH="$1"

    # Must use absolute path to which(1) because RedHat is dumb
    STAT=$(/usr/bin/which stat 2>/dev/null)
    if [ -z "$STAT" ]
    then
        _fake_stat "$DISKPATH"
    else
        $STAT -c '0x%t 0x%T' "$DISKPATH" 2>/dev/null
    fi
}

print_dev()
{
    if [ "$#" -lt "1" -o -z "$1" ]
    then
        die "print_dev(): Requires an argument"
    fi

    MAJOR_MINOR="$(get_dev "$1")"

    # MAJOR_MINOR cannot be quoted, because we want the space
    printf "%d %d" $MAJOR_MINOR
}

# Adding timestamp
add_timestmp()
{
    while read -r line; do
        printf "%s %s\n" "$(date '+%a %b %e %H:%M:%S')" "$line"
    done
}

#
# Log command output and errors to file: /var/log/oracleasm
# asm_log 1: Send the output to stdout and the logfile: /var/log/oracleasm
# asm_log 2: Send the output only to logfile: /var/log/oracleasm
#

asm_log()
{
    while read -r line; do
        if [ "$1" -eq "1" ]
        then
               echo "$line"
        fi
        echo "$line" |add_timestmp >>/var/log/oracleasm
    done
}

#
# Check if the host kernel supports ASM driver.
# NOTE: Starting with UEK7 kernel no ASM driver is supported.
#
asm_driver_check()
{
    # If ORACLEASM_USE_DRIVER=false then assume no
    # ASM driver is present
    if [ "$ORACLEASM_USE_DRIVER" = "false" ]
    then
	ORACLEASM_DRIVER_SUPPORTED=false
	return
    fi

    # Check if ASM driver is present in the system
    if [ -z "$ORACLE_ASMMANAGER" ]
    then
        modinfo oracleasm >/dev/null 2>&1
    else
        modinfo $(basename $ORACLE_ASMMANAGER) >/dev/null 2>&1
    fi

    if [ $? != 0 ]
    then
        ORACLEASM_DRIVER_SUPPORTED=false
    else
        ORACLEASM_DRIVER_SUPPORTED=true
    fi
}

# Used when ASM driver is not supported in the kernel.
# This sets up BPF iofilter for providing exclusive access
# to IO operations initiated by ASMlibv3. This works
# starting with UEK7 kernel only.
iofilter_init ()
{
    if [ "$ORACLEASM_ENABLE_IOFILTER" != "true" ]
    then
	return
    fi

    # Setup and pin iofilter program
    echo -n "Setting up iofilter map for ASM disks: "

    # XXX Check if iofilter_asm program is available
    if [ ! -f "${IOFILTER_ASM}" ]
    then
	echo "failed -- no iofilter_asm program available" | asm_log 1
	return 1
    fi

    if [ -e "${ORACLEASM_IOFILTER_MAP_PATH}" ]
    then
	# map already exists
	echo "done"
	return 0
    fi

    # iofilter map size should be big enough to accommodate the max number
    # of ASM disks that may be configured into the system. To accommodate
    # multiple paths (upto 8) in the case of multipathed disks a factor
    # of 10 is used. This also includes additional entries needed for whole
    # disk entries of each ASM disk which could be a disk partition.
    if [ -z "${ORACLEASM_CONFIG_MAX_DISKS}" ]
    then
	mapsize="$(expr 2048 \* 10)"
    else
    	mapsize="$(expr ${ORACLEASM_CONFIG_MAX_DISKS} \* 10)"
    fi

    # setup the iofilter map
    ${IOFILTER_ASM} --init --pin "$ORACLEASM_IOFILTER_MAP_PATH" \
	    --maxdev "$mapsize" 2>&3
    if [ $? != 0 ]
    then
	echo "Failed to setup iofilter map" | asm_log 1
        return 1
    else
	echo "done"
    fi

    return 0
}

#
# Add specified ASM disk to iofilter map so IO is monitored.
# This includes adding any (multiple) paths (i.e. <major,minor>)
# associated with this disk. This function is used when adding
# an ASM disk (createdisk, renamedisk and scandisks operations).
#
# NOTES: This works only from UEK7 kernel versions that has BPF 
# infrastructure needed for the iofilter program to work.
#
iofilter_map_add ()
{
    if [ "$ORACLEASM_ENABLE_IOFILTER" != "true" ]
    then
	return 0
    fi

    # XXX Check if iofilter_asm program is available
    if [ ! -f "${IOFILTER_ASM}" ]
    then
	return  0
    fi

    if [ "$#" -lt "1" -o -z "$1" ]
    then
        echo "iofilter_map_add: Requires an argument" | asm_log 2
	return 0
    fi

    # This disk could be a multipath disk so there may be
    # multiple paths including DM device (/dev/mapper) path.
    disk_paths=$(list_disk_paths "${ORACLE_ASMMANAGER}" "$1" 2>&3)
    if [ -z "$disk_paths" ]
    then
	echo "iofilter_map_add: invalid argument $1" | asm_log 2
	return 0
    fi

    dm_path=""

    for disk_path in $(echo $disk_paths)
    do
	#
        # Get the disk info (type, size, start sector for partition type).
	#

    	# Get disk type (disk or partition)
    	disk_type=$(lsblk -dn -o type ${disk_path})

    	# Get size of the disk
    	disk_size=$(blockdev --getsz ${disk_path})
    	if [ $? != 0 ]
    	then
	    echo "iofilter_map_add: failed to get disk size for $1" | \
		asm_log 2
	    return 1
    	fi
    	range_end_sec=$(expr $disk_size - 1)

        if [ "${disk_type}" = "part" ]
	then
	    dev_dir=$(dirname $disk_path)
            if [ "${dev_dir}" = "/dev/mapper" ]
	    then
		dm_path=$disk_path
	        info=$(dmsetup table $disk_path 2>/dev/null)
	    else
		dev=$(basename ${disk_path})
		info=$(blockdev --report ${disk_path} 2>/dev/null | grep "$dev")
            fi
	    if [ -z "${info}" ]
	    then
	        echo "iofilter_map_add: failed to get disk offset $1" | \
		    asm_log 2
	        return 1
	    else
	        offset="$(echo $info | awk '{print $5}')"
	    fi
	else
	    offset=0
	fi

        # Add the disk to the BPF filter map.
	# NOTE: For DM device path there is no physical block device parent
	# associated with it so no parent device is to be updated.
        if [ "${disk_type}" != "part" -o "${disk_path}" = "${dm_path}" ]
        then
	    # it is a whole disk or DM path device
	    ${IOFILTER_ASM} --add "${disk_path}" --range "0-${range_end_sec}" \
	        --offset "${offset}" --type write \
	        --pin "${ORACLEASM_IOFILTER_MAP_PATH}" | asm_log 1
        else
	    # It is a disk partition for a regular block device, parent 
	    # disk entry also gets updated.
	    parent_disk=$(lsblk -dn -o PKNAME ${disk_path})
	    if [ -z "${parent_disk}" ]
	    then
		echo "iofilter_map_add: invalid diskpath $disk_path" | asm_log 1
		return 1
	    fi
	    ${IOFILTER_ASM} --add "${disk_path}" --range "0-${range_end_sec}" \
		    --offset "${offset}" --type write \
		    --pin "${ORACLEASM_IOFILTER_MAP_PATH}" \
		    --parent "/dev/${parent_disk}" | asm_log 1
        fi

        if [ ${PIPESTATUS[0]} != 0 ]
        then
	    #DEBUG echo "Failed to add disk ${disk_path} to iofilter map"
            return 1
        fi

        echo "Added disk ${disk_path} to iofilter map" | asm_log 2
    done

    return 0
}

#
# Delete the specified ASM disk from iofilter map.
# This includes deleting any (multiple) paths (i.e. <major,minor>)
# associated with this disk from the filter map. This function is
# used when deleting an ASM disk (deletedisk and renamedisk operations).
#
# NOTES: This works only from UEK7 kernel versions that has BPF 
# infrastructure needed for the iofilter program to work.
#
iofilter_map_delete ()
{
    if [ "$ORACLEASM_ENABLE_IOFILTER" != "true" ]
    then
	return 0
    fi

    # XXX Check if iofilter_asm program is available
    if [ ! -f "${IOFILTER_ASM}" ]
    then
	return  0
    fi

    if [ "$#" -lt "1" -o -z "$1" ]
    then
        echo "iofilter_map_delete(): Requires an argument"
	return 0
    fi

    # This disk could be a multipath disk 
    disk_paths=$(list_disk_paths "${ORACLE_ASMMANAGER}" "$1" 2>&3)
    if [ -z "$disk_paths" ]
    then
	echo "iofilter_map_delete: invalid argument $1" | asm_log 2
	return 0
    fi

    dm_path=""
    for disk_path in $(echo $disk_paths)
    do
        # Get disk type (disk or partition)
        disk_type=$(lsblk -dn -o type ${disk_path})

	# check if this is DM device path
	dev_dir=$(dirname $disk_path)
        if [ "${dev_dir}" = "/dev/mapper" ]
	then
	    dm_path=$disk_path
	fi

	# For the whole disk or for the DM device path just delete
	# the entry in the iofilter map. If it is a physical disk
	# partition then the parent entry is also to be updated
	# in the map.

        if [ "${disk_type}" != "part" -o "${disk_path}" = "${dm_path}" ]
        then
	    # regular whole disk or DM device
            ${IOFILTER_ASM} --delete "${disk_path}" --pin \
		"${ORACLEASM_IOFILTER_MAP_PATH}" --silent | asm_log 1
        else
	    parent_disk=$(lsblk -no PKNAME ${disk_path})
            ${IOFILTER_ASM} --delete "${disk_path}" --pin \
		"${ORACLEASM_IOFILTER_MAP_PATH}" --parent \
		"/dev/${parent_disk}" --silent | asm_log 1
        fi

        if [ ${PIPESTATUS[0]} != 0 ]
        then
            #DEBUG echo "Failed to delete disk ${disk_path} from iofilter map"
            return 1
        fi

        echo "Removed disk ${disk_path} from iofilter map" | asm_log 2
    done

    return 0
}

#
# Check if io_uring (used with UEK7+ kernel that doesn't have oracleasm
# driver) is disabled.
#
# Note: sysctl parameter kernel.io_uring_disabled is available only
# with newer UEK kernels and in the absence of this parameter
# it assumes io_uring is enabled.
#
check_io_uring_disabled()
{
    io_uring_disabled=$(sysctl -n kernel.io_uring_disabled 2>/dev/null)
    if [ $? != 0 ]
    then
	return 0
    fi
    if [ "$io_uring_disabled" -eq 2 ]
    then
	return 1
    else
	return 0
    fi
}

#
# Load configuration
#

ORACLEASM_MODNAME="oracleasm"
ORACLEASM_CONFIG_DIR=/etc/sysconfig
ORACLEASM_IOFILTER_MAP_PATH="/sys/fs/bpf/iofilter_asm"
IOFILTER_ASM="/usr/lib/oracleasm/iofilter_asm"

if [ -z "$ORACLE_ASMMANAGER" ]
then
    ORACLE_ASMMANAGER="/dev/oracleasm"
fi

if [ "$ORACLE_ASMMANAGER" = "/dev/oracleasm" ]
then
    ORACLEASM_CONFIG_FALLBACK="oracleasm"
else
    ORACLEASM_CONFIG_FALLBACK=
fi

ORACLEASM_CONFIG_FILE="oracleasm-$(echo ${ORACLE_ASMMANAGER} | sed -e 's/\//_/g')"


if [ ! -e "$ORACLEASM_CONFIG_DIR" ]
then
    mkdir -p "$ORACLEASM_CONFIG_DIR" 2>&3
    if [ $? != 0 ]
    then
        die "Unable to access configuration directory"
    fi
fi

ORACLEASM_CONFIG="${ORACLEASM_CONFIG_DIR}/${ORACLEASM_CONFIG_FILE}"
if [ -n "$ORACLEASM_CONFIG_FALLBACK" -a ! -f "${ORACLEASM_CONFIG}" -a \
     -f "${ORACLEASM_CONFIG_DIR}/${ORACLEASM_CONFIG_FALLBACK}" ]
then
    mv "${ORACLEASM_CONFIG_DIR}/${ORACLEASM_CONFIG_FALLBACK}" \
        "${ORACLEASM_CONFIG}" 2>&3
    if [ $? != 0 ]
    then
        die "Unable to update configuration linkage"
    fi

    ln -s "${ORACLEASM_CONFIG_FILE}" \
        "${ORACLEASM_CONFIG_DIR}/${ORACLEASM_CONFIG_FALLBACK}" \
        2>&3
    if [ $? != 0 ]
    then
        die "Unable to update configuration linkage"
    fi
elif [ -f "${ORACLEASM_CONFIG}" -a ! -L "${ORACLEASM_CONFIG_DIR}/${ORACLEASM_CONFIG_FALLBACK}" ]
then
        rm -f "${ORACLEASM_CONFIG_DIR}/${ORACLEASM_CONFIG_FALLBACK}" 2>&3
        ln -s "${ORACLEASM_CONFIG}" "${ORACLEASM_CONFIG_DIR}/${ORACLEASM_CONFIG_FALLBACK}" 2>&3
fi

[ -f "${ORACLEASM_CONFIG}" ] && . "${ORACLEASM_CONFIG}"

asm_driver_check
