#!/usr/bin/bash -x
#
# Copyright (c) 2006, 2025, 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.1.1"
}


#
# 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 "$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()
{
    name="$1"

    if [ "$ORACLEASM_DRIVER_SUPPORTED" = "true" ]
    then
	echo "${ORACLE_ASMMANAGER}/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
        lsblk -o fstype,label|grep oracleasm|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()
{
    name="$1"

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

    # get the disk paths for the specified ASM disk (label)
    # which may be a multipath disk
    lsblk -flpn | grep -w $name | sort -u | awk '{print $1}'
}

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

    DISK="$1"

    MEMBER="$(oracleasm-read-label -m "${DISK}" 2>&1 | sed 's/.*: //')"
    if [ $? != 0 ]
    then
	echo "Unable to access \"${DISK}\"" | asm_log 1
	exit 1
    fi

    if [ -n "$MEMBER" ]
    then
	echo ""
	echo "WARNING: $MEMBER" | asm_log 1
	cat <<EOCAT

WARNING: Changing the label of an disk marked for ASM is a very dangerous
         operation.  If this is really what you mean to do, you must
         ensure that all Oracle and ASM instances have ceased using
         this disk.  Otherwise, you may LOSE DATA.

If you are really sure you wish to change the label and are sure that
all of your Oracle and ASM instances have ceased using the disk,
EOCAT
	if [ "$2" == "createdisk" ]
	then
	    echo "please run 'oracleasm deletedisk' before creating" \
		 "a new disk label."
	    echo ""
	else
	    echo "rerun this command with the '-f' option."
	    echo ""
	fi
	exit 1
    fi
}

# 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_driver()
{
    # If ORACLEASM_USE_DRIVER=false is specified in config file,
    # assume no oracleasm driver is present.
    if [ "$ORACLEASM_USE_DRIVER" = "false" ]
    then
	return
    fi

    # Check if oracleasm driver module is available
    modinfo oracleasm >/dev/null 2>&1
    if [ $? == 0 ]
    then
	ORACLEASM_DRIVER_SUPPORTED=true
	return
    fi

    echo "ERROR: oracleasm kernel driver required but not found" | asm_log 1
    exit 1
}

check_kernel()
{
    if [ -f /etc/os-release ]
    then
	. /etc/os-release
    fi

    if [ -z "${CPE_NAME}" ]
    then
	echo "Unknown Linux distribution" | asm_log 1
	return
    fi

    UNAME=`uname -r`
    ARCH=`uname -m`
    KERNEL_NAME="Linux ${UNAME}"

    case ${CPE_NAME} in
	cpe:/o:oracle:linux:10:*)
	    case ${UNAME} in
		6.12.0-*el10uek*)
		    KERNEL_NAME="UEK8"
		    ORACLEASM_IOFILTER_SUPPORTED=true
		    ;;
		6.12.0-*el10*)
		    KERNEL_NAME="RHCK10"
		    ;;
	    esac
	    ;;
	cpe:/o:oracle:linux:9:*)
	    case ${UNAME} in
		6.12.0-*el9uek*)
		    KERNEL_NAME="UEK8"
		    ORACLEASM_IOFILTER_SUPPORTED=true
		    ;;
		5.15.0*el9uek*)
		    KERNEL_NAME="UEK7"
		    ORACLEASM_IOFILTER_SUPPORTED=true
		    ;;
		5.14.0-*el9*)
		    KERNEL_NAME="RHCK9"
		    if [ ${ARCH} = "x86_64" ]; then
			ORACLEASM_IOFILTER_SUPPORTED=true
		    fi
		    ;;
	    esac
	    ;;
	cpe:/o:oracle:linux:8:*)
	    case ${UNAME} in
		5.15.0*el8uek*)
		    KERNEL_NAME="UEK7"
		    ORACLEASM_IOFILTER_SUPPORTED=true
		    ;;
		5.4.17-*el8uek*)
		    KERNEL_NAME="UEK6"
		    check_driver
		    ;;
		4.18.0-*el8*)
		    KERNEL_NAME="RHCK8"
		    check_driver
		    ;;
	    esac
	    ;;
	cpe:/o:redhat:enterprise_linux:10:*)
	    case ${UNAME} in
		6.12.0-*el10*)
		    KERNEL_NAME="RHEL10"
		    ;;
	    esac
	    ;;
	cpe:/o:redhat:enterprise_linux:9:*)
	    case ${UNAME} in
		5.14.0-*el9*)
		    KERNEL_NAME="RHEL9"
		    if [ ${ARCH} = "x86_64" ]; then
			ORACLEASM_IOFILTER_SUPPORTED=true
		    fi
		    ;;
	    esac
	    ;;
	cpe:/o:redhat:enterprise_linux:8:*)
	    case ${UNAME} in
		4.18.0-*el8*)
		    KERNEL_NAME="RHEL8"
		    check_driver
		    ;;
	    esac
	    ;;
	cpe:/o:suse:sles:16:*)
	    case ${UNAME} in
		6.12.0-16*)
		    KERNEL_NAME="SLES16"
		    if [ ${ARCH} = "x86_64" ]; then
			ORACLEASM_IOFILTER_SUPPORTED=true
		    fi
		    ;;
	    esac
	    ;;
	cpe:/o:suse:sles:15:*)
	    case ${UNAME} in
		6.4.0-15*)
		    KERNEL_NAME="SLES15"
		    check_driver
		    ;;
	    esac
	    ;;
    esac
}

check_iofilter()
{
    if [ "${ORACLEASM_ENABLE_IOFILTER}" == "false" ]; then
	return
    fi

    if [ "${ORACLEASM_IOFILTER_SUPPORTED}" == "false" ]; then
	echo ""
	echo "WARNING: I/O filtering not supported with ${KERNEL_NAME} on ${ARCH}" | asm_log 1
	echo ""
    fi
}

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

    check_iofilter

    if [ "$ORACLEASM_IOFILTER_SUPPORTED" != "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" --uid $(/usr/bin/id -u ${ORACLEASM_UID:-root}) 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

    check_iofilter

    if [ "$ORACLEASM_IOFILTER_SUPPORTED" != "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 "$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

    if [ "$ORACLEASM_IOFILTER_SUPPORTED" != "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 "$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
}

# Validate that the oracleasm config file exists and the systemd
# service is enabled and running.
check_oracleasm_config()
{
    # If we were called from oracleasm.service, skip the sanity checks
    # since the service is in the process of being brought up.
    if [ -n "${ORACLEASM_SERVICE}" ]
    then
	return
    fi

    if [ ! -r "${ORACLEASM_CONFIG}" ]
    then
	echo "${ORACLEASM_CONFIG} not found" | asm_log 1
	echo "Please run 'oracleasm configure -i' to configure oracleasm" | asm_log 1
	exit 1
    fi

    if [ `systemctl is-enabled oracleasm.service` != "enabled" ]
    then
	echo "oracleasm.service not enabled" | asm_log 1
	echo "Please run 'oracleasm configure -e' to enable oracleasm" | asm_log 1
	exit 1
    fi

    if [ `systemctl is-active oracleasm.service` != "active" ]
    then
	echo "oracleasm.service not started" | asm_log 1
	echo "Please run 'systemctl start oracleasm.service' to start oracleasm" | asm_log 1
	exit 1
    fi

    if [ ! -r "${ASMLIB_PATH}" ]
    then
	echo "Oracle ASM Library not found" | asm_log 1
	echo "Please download and install the oracleasmlib package" | asm_log 1
	exit 1
    fi
}

#
# Load configuration
#

ASMLIB_PATH="/opt/oracle/extapi/64/asm/orcl/1/libasm.so"
ORACLEASM_MODNAME="oracleasm"
ORACLEASM_CONFIG_DIR=/etc/sysconfig
ORACLEASM_IOFILTER_MAP_PATH="/sys/fs/bpf/iofilter_asm"
IOFILTER_ASM="/usr/lib/oracleasm/iofilter_asm"
ORACLE_ASMMANAGER="/dev/oracleasm"
ORACLEASM_CONFIG_FALLBACK="oracleasm"
ORACLEASM_CONFIG_FILE="oracleasm-$(echo ${ORACLE_ASMMANAGER} | sed -e 's/\//_/g')"
ORACLEASM_CMD=/usr/sbin/oracleasm
ORACLEASM_DRIVER_SUPPORTED=false
ORACLEASM_IOFILTER_SUPPORTED=false

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}"

check_kernel
