#!/bin/bash
#
# Copyright (C) 2022-2025 Red Hat, Inc. All rights reserved.
#
# This file is part of LVM2.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

set -euE -o pipefail

PATH="/sbin:/usr/sbin:/bin:/usr/bin:$PATH"
GETOPT="getopt"
SCRIPTNAME=$(basename "$0")
DM_DEV_DIR="${DM_DEV_DIR:-/dev}"

usage() {
	cat <<-EOF
	  ${SCRIPTNAME}: helper script called by lvresize to resize file systems.

	  ${SCRIPTNAME} --fsextend --fstype name --lvpath path
	      [ --mountdir path ]
	      [ --mount ]
	      [ --unmount ]
	      [ --remount ]
	      [ --fsck ]
	      [ --cryptresize ]
	      [ --cryptpath path ]
	      [ --newsizebytes num ]

	  ${SCRIPTNAME} --fsreduce --fstype name --lvpath path
	      [ --newsizebytes num ]
	      [ --mountdir path ]
	      [ --mount ]
	      [ --unmount ]
	      [ --remount ]
	      [ --fsck ]
	      [ --cryptresize ]
	      [ --cryptpath path ]

	  ${SCRIPTNAME} --cryptresize --cryptpath path --newsizebytes num

	  Options:
	      --fsextend
		  Extend the file system.
	      --fsreduce
		  Reduce the file system.
	      --fstype name
		  The type of file system (ext*, xfs, btrfs.)
	      --lvpath path
		  The path to the LV being resized.
	      --mountdir path
		  The file system is currently mounted here.
	      --mount
		  Mount the file system on a temporary directory before resizing.
	      --unmount
		  Unmount the file system before resizing.
	      --remount
		  Remount the file system after resizing if unmounted.
	      --fsck
		  Run fsck on the file system before resizing (only with ext*).
	      --newsizebytes num
		  The new size of the file system.
	      --cryptresize
		  Resize the crypt device between the LV and file system.
	      --cryptpath path
		  The path to the crypt device.

	EOF

	exit 0
}

errorexit() {
	echo "$1" >&2
	exit 1
}

logerror() {
	echo "$1" >&2
	logger "${SCRIPTNAME}: $1"
}

logmsg() {
	echo "$1"
	logger "${SCRIPTNAME}: $1"
}

# Handle e2fsck return codes according to fsck(8) exit code specification
# 0 = no errors, 1 = errors corrected, 2 = reboot required
# 4 = errors left uncorrected, 8 = operational error
# 16 = usage error, 32 = canceled, 128 = shared library error
accept_e2fsck() {
	local ret=0
	"$@" || ret=$?

	case "$ret" in
	  0)
		# No errors
		logmsg "e2fsck done"
		return 0
		;;
	  1)
		# Filesystem was corrected
		logmsg "e2fsck done (filesystem errors were corrected)"
		return 0
		;;
	  2)
		# System should be rebooted
		logerror "WARNING: Filesystem was corrected but system should be rebooted"
		logmsg "e2fsck done (reboot recommended)"
		return 0
		;;
	  4)
		logerror "e2fsck failed: filesystem errors left uncorrected on \"$DEVPATH\""
		exit 1
		;;
	  8)
		logerror "e2fsck failed: operational error on \"$DEVPATH\""
		exit 1
		;;
	  16)
		logerror "e2fsck failed: usage or syntax error"
		exit 1
		;;
	  32)
		logerror "e2fsck canceled by user"
		exit 1
		;;
	  *)
		logerror "e2fsck failed with return code $ret on \"$DEVPATH\""
		exit 1
		;;
	esac
}

btrfs_path_major_minor() {
	local STAT

	STAT=$(stat --format "echo \$((0x%t)):\$((0x%T))" "$(readlink -e "$1")") || \
		errorexit "Cannot get major:minor for \"$1\"."
	eval "$STAT"
}

btrfs_devid() {
	local devpath=$1
	local devid devinfo major_minor path_major_minor
	local IFS=$'\n'

	major_minor=$(btrfs_path_major_minor "$devpath")

	# It could be a multi-devices btrfs, filter the output.
	# Device in `btrfs filesystem show $devpath` could be /dev/mapper/* so call `readlink -e`
	for devinfo in $(LC_ALL=C btrfs filesystem show "$devpath"); do
		case "$devinfo" in
		*devid*)
			path_major_minor=$(btrfs_path_major_minor "${devinfo#* path }")
			# compare Major:Minor
			[ "$major_minor" = "$path_major_minor" ] || continue
			devid=${devinfo##*devid}
			devid=${devid%%size*}

			# trim all prefix and postfix spaces from devid
			devid=${devid#"${devid%%[![:space:]]*}"}
			echo "${devid%"${devid##*[![:space:]]}"}"
			return 0
			;;
		esac
	done

	# fail, devid not found
	return 1
}

# Set to 1 while the fs is temporarily mounted on $TMPDIR
TMP_MOUNT_DONE=0
# Set to 1 if the fs resize command fails
RESIZEFS_FAILED=0

# Function to detect XFS mount options
detect_xfs_mount_options() {
	local device=$1
	local qflags_output qflags_hex
	local prefix acct_flag enfd_flag
	local -a opts=()
	MOUNT_OPTIONS=""

	# Get quota flags using xfs_db.
	if ! qflags_output=$(xfs_db -r "$device" -c 'sb 0' -c 'p qflags'); then
		logerror "xfs_db failed to read quota flags from \"$device\""
		return 1
	fi

	# Extract the hex value from output that is in format "qflags = 0x<hex_number>".
	qflags_hex="${qflags_output#qflags = }"

	# No flags set, no extra mount options needed.
	[[ "$qflags_hex" == "0" ]] && return 0

	if [[ ! "$qflags_hex" =~ ^0x[0-9a-fA-F]+$ ]]; then
		logerror "xfs_db unexpected output for \"$device\": got \"$qflags_hex\""
		return 1
	fi

	# Check XFS quota flags and build mount options
	# The quota flags as defined in Linux kernel source: fs/xfs/libxfs/xfs_log_format.h:
	#   XFS_UQUOTA_ACCT = 0x0001, XFS_UQUOTA_ENFD = 0x0002
	#   XFS_GQUOTA_ACCT = 0x0040, XFS_GQUOTA_ENFD = 0x0080
	#   XFS_PQUOTA_ACCT = 0x0008, XFS_PQUOTA_ENFD = 0x0200
	#
	# Format: "prefix acct_flag enfd_flag"
	for quota_type in "u 0x0001 0x0002" "g 0x0040 0x0080" "p 0x0008 0x0200"; do
		read -r prefix acct_flag enfd_flag <<< "$quota_type"
		if [ $((qflags_hex & acct_flag)) -ne 0 ]; then
			if [ $((qflags_hex & enfd_flag)) -ne 0 ]; then
				opts+=("${prefix}quota")
			else
				opts+=("${prefix}qnoenforce")
			fi
		fi
	done

	# Join array elements with commas
	MOUNT_OPTIONS=$(IFS=,; echo "${opts[*]}")

	[[ -n "$MOUNT_OPTIONS" ]] && logmsg "mount options for xfs: ${MOUNT_OPTIONS}"
}

fsextend() {
	if [ "$DO_UNMOUNT" -eq 1 ]; then
		logmsg "unmount ${MOUNTDIR}"
		if umount "$MOUNTDIR"; then
			logmsg "unmount done"
		else
			logerror "unmount failed for \"$MOUNTDIR\""
			exit 1
		fi
	fi

	if [ "$DO_FSCK" -eq 1 ]; then
		if [[ "$FSTYPE" == "ext"* ]]; then
			logmsg "e2fsck ${DEVPATH}"
			accept_e2fsck e2fsck -f -p "$DEVPATH"
		elif [[ "$FSTYPE" == "btrfs" ]]; then
			logmsg "btrfs check ${DEVPATH}"
			if btrfs check "$DEVPATH"; then
				logmsg "btrfs check done"
			else
				logerror "btrfs check failed on \"$DEVPATH\""
				exit 1
			fi
		fi
	fi

	if [ "$DO_CRYPTRESIZE" -eq 1 ]; then
		logmsg "cryptsetup resize ${DEVPATH}"
		if cryptsetup resize "$DEVPATH"; then
			logmsg "cryptsetup done"
		else
			logerror "cryptsetup resize failed on \"$DEVPATH\""
			exit 1
		fi
	fi

	if [ "$DO_MOUNT" -eq 1 ]; then
		if [[ "$FSTYPE" == "xfs" ]]; then
			detect_xfs_mount_options "$DEVPATH" || logmsg "not using XFS mount options"
		fi

		logmsg "mount ${DEVPATH} ${TMPDIR}"
		if mount -t "$FSTYPE" ${MOUNT_OPTIONS:+-o "$MOUNT_OPTIONS"} "$DEVPATH" "$TMPDIR"; then
			logmsg "mount done"
			TMP_MOUNT_DONE=1
		else
			logerror "mount failed for \"$DEVPATH\" on \"$TMPDIR\""
			exit 1
		fi
	fi

	if [[ "$FSTYPE" == "ext"* ]]; then
		logmsg "resize2fs ${DEVPATH}"
		if resize2fs "$DEVPATH"; then
			logmsg "resize2fs done"
		else
			logerror "resize2fs failed on \"$DEVPATH\""
			RESIZEFS_FAILED=1
		fi
	elif [[ "$FSTYPE" == "xfs" ]]; then
		logmsg "xfs_growfs ${DEVPATH}"
		if xfs_growfs "$DEVPATH"; then
			logmsg "xfs_growfs done"
		else
			logerror "xfs_growfs failed on \"$DEVPATH\""
			RESIZEFS_FAILED=1
		fi
	elif [[ "$FSTYPE" == "btrfs" ]]; then
		NEWSIZEBYTES=${NEWSIZEBYTES:-max}
		BTRFS_DEVID="$(btrfs_devid "$DEVPATH")"
		REAL_MOUNTPOINT="$MOUNTDIR"

		if [ $TMP_MOUNT_DONE -eq 1 ]; then
			REAL_MOUNTPOINT="$TMPDIR"
		fi

		logmsg "btrfs filesystem resize ${BTRFS_DEVID}:${NEWSIZEBYTES} ${REAL_MOUNTPOINT}"
		if btrfs filesystem resize "$BTRFS_DEVID":"$NEWSIZEBYTES" "$REAL_MOUNTPOINT"; then
			logmsg "btrfs filesystem resize done"
		else
			logerror "btrfs filesystem resize failed: devid $BTRFS_DEVID to $NEWSIZEBYTES on \"$REAL_MOUNTPOINT\""
			RESIZEFS_FAILED=1
		fi
	fi

	# If the fs was temporarily mounted, now unmount it.
	if [ $TMP_MOUNT_DONE -eq 1 ]; then
		logmsg "cleanup unmount ${TMPDIR}"
		if umount "$TMPDIR"; then
			logmsg "cleanup unmount done"
			TMP_MOUNT_DONE=0
			rmdir "$TMPDIR"
		else
			logerror "cleanup unmount failed for \"$TMPDIR\""
			exit 1
		fi
	fi

	# If the fs was temporarily unmounted, now remount it.
	# Not considered a command failure if this fails.
	if [[ $DO_UNMOUNT -eq 1 && $REMOUNT -eq 1 ]]; then
		if [[ "$FSTYPE" == "xfs" ]]; then
			detect_xfs_mount_options "$DEVPATH" || logmsg "not using XFS mount options"
		fi

		logmsg "remount ${DEVPATH} ${MOUNTDIR}"
		if mount -t "$FSTYPE" ${MOUNT_OPTIONS:+-o "$MOUNT_OPTIONS"} "$DEVPATH" "$MOUNTDIR"; then
			logmsg "remount done"
		else
			logmsg "remount failed"
		fi
	fi

	if [ $RESIZEFS_FAILED -eq 1 ]; then
		logerror "File system extend failed."
		exit 1
	fi

	exit 0
}

fsreduce() {
	if [ "$DO_UNMOUNT" -eq 1 ]; then
		logmsg "unmount ${MOUNTDIR}"
		if umount "$MOUNTDIR"; then
			logmsg "unmount done"
		else
			logerror "unmount failed for \"$MOUNTDIR\""
			exit 1
		fi
	fi

	if [ "$DO_FSCK" -eq 1 ]; then
		if [[ "$FSTYPE" == "ext"* ]]; then
			logmsg "e2fsck ${DEVPATH}"
			accept_e2fsck e2fsck -f -p "$DEVPATH"
		elif [[ "$FSTYPE" == "btrfs" ]]; then
			logmsg "btrfs check ${DEVPATH}"
			if btrfs check "$DEVPATH"; then
				logmsg "btrfs check done"
			else
				logerror "btrfs check failed on \"$DEVPATH\""
				exit 1
			fi
		fi
	fi

	if [ "$DO_MOUNT" -eq 1 ]; then
		logmsg "mount ${DEVPATH} ${TMPDIR}"
		if mount -t "$FSTYPE" "$DEVPATH" "$TMPDIR"; then
			logmsg "mount done"
			TMP_MOUNT_DONE=1
		else
			logerror "mount failed for \"$DEVPATH\" on \"$TMPDIR\""
			exit 1
		fi
	fi

	if [[ "$FSTYPE" == "ext"* ]]; then
		NEWSIZEKB=$(( NEWSIZEBYTES / 1024 ))
		logmsg "resize2fs ${DEVPATH} ${NEWSIZEKB}k"
		if resize2fs "$DEVPATH" "$NEWSIZEKB"k; then
			logmsg "resize2fs done"
		else
			logerror "resize2fs failed on \"$DEVPATH\" to ${NEWSIZEKB}k"
			# will exit after cleanup unmount
			RESIZEFS_FAILED=1
		fi
	elif [[ "$FSTYPE" == "btrfs" ]]; then
		BTRFS_DEVID="$(btrfs_devid "$DEVPATH")"
		REAL_MOUNTPOINT="$MOUNTDIR"

		if [ $TMP_MOUNT_DONE -eq 1 ]; then
			REAL_MOUNTPOINT="$TMPDIR"
		fi

		logmsg "btrfs filesystem resize ${BTRFS_DEVID}:${NEWSIZEBYTES} ${REAL_MOUNTPOINT}"
		if btrfs filesystem resize "$BTRFS_DEVID":"$NEWSIZEBYTES" "$REAL_MOUNTPOINT"; then
			logmsg "btrfs filesystem resize done"
		else
			logerror "btrfs filesystem resize failed: devid $BTRFS_DEVID to $NEWSIZEBYTES on \"$REAL_MOUNTPOINT\""
			RESIZEFS_FAILED=1
		fi
	fi

	# If the fs was temporarily mounted, now unmount it.
	if [ $TMP_MOUNT_DONE -eq 1 ]; then
		logmsg "cleanup unmount ${TMPDIR}"
		if umount "$TMPDIR"; then
			logmsg "cleanup unmount done"
			TMP_MOUNT_DONE=0
			rmdir "$TMPDIR"
		else
			logerror "cleanup unmount failed for \"$TMPDIR\""
			exit 1
		fi
	fi

	if [ $RESIZEFS_FAILED -eq 1 ]; then
		logerror "File system reduce failed."
		exit 1
	fi

	if [ "$DO_CRYPTRESIZE" -eq 1 ]; then
		NEWSIZESECTORS=$(( NEWSIZEBYTES / 512 ))
		logmsg "cryptsetup resize ${NEWSIZESECTORS} sectors ${DEVPATH}"
		if cryptsetup resize --size "$NEWSIZESECTORS" "$DEVPATH"; then
			logmsg "cryptsetup done"
		else
			logmsg "cryptsetup failed"
			exit 1
		fi
	fi

	# If the fs was temporarily unmounted, now remount it.
	# Not considered a command failure if this fails.
	if [[ $DO_UNMOUNT -eq 1 && $REMOUNT -eq 1 ]]; then
		logmsg "remount ${DEVPATH} ${MOUNTDIR}"
		if mount -t "$FSTYPE" "$DEVPATH" "$MOUNTDIR"; then
			logmsg "remount done"
		else
			logmsg "remount failed"
		fi
	fi

	exit 0
}

cryptresize() {
	NEWSIZESECTORS=$(( NEWSIZEBYTES / 512 ))
	logmsg "cryptsetup resize ${NEWSIZESECTORS} sectors ${DEVPATH}"
	if cryptsetup resize --size "$NEWSIZESECTORS" "$DEVPATH"; then
		logmsg "cryptsetup done"
	else
		logerror "cryptsetup resize failed on \"$DEVPATH\" to $NEWSIZESECTORS sectors"
		exit 1
	fi

	exit 0
}

#
# BEGIN SCRIPT
#

# These are the only commands that this script will run.
# Each is enabled (1) by the corresponding command options:
# --fsextend, --fsreduce, --cryptresize, --mount, --unmount, --fsck
DO_FSEXTEND=0
DO_FSREDUCE=0
DO_CRYPTRESIZE=0
DO_MOUNT=0
DO_UNMOUNT=0
DO_FSCK=0

# --remount: attempt to remount the fs if it was originally
# mounted and the script unmounted it.
REMOUNT=0

# Initialize MOUNT_OPTIONS to ensure clean state
MOUNT_OPTIONS=""
MOUNTDIR=""

OPTIONS=$("$GETOPT" -o h -l help,fsextend,fsreduce,cryptresize,mount,unmount,remount,fsck,fstype:,lvpath:,newsizebytes:,mountdir:,cryptpath: -n "${SCRIPTNAME}" -- "$@")
eval set -- "$OPTIONS"

while [ $# -gt 0 ]
do
	case $1 in
	--fsextend)	DO_FSEXTEND=1 ;;
	--fsreduce)	DO_FSREDUCE=1 ;;
	--cryptresize)	DO_CRYPTRESIZE=1 ;;
	--mount)	DO_MOUNT=1 ;;
	--unmount)	DO_UNMOUNT=1 ;;
	--fsck)		DO_FSCK=1 ;;
	--remount)	REMOUNT=1 ;;
	--fstype)	FSTYPE=$2; shift ;;
	--lvpath)	LVPATH=$2; shift ;;
	--newsizebytes)	NEWSIZEBYTES=$2; shift ;;
	--mountdir)	MOUNTDIR=$2; shift ;;
	--cryptpath)	CRYPTPATH=$2; shift ;;
	-h|--help)	usage ;;
	--)		shift; break ;;
	*)		errorexit "Unknown option \"$1\"." ;;
	esac
	shift
done

if [ "$UID" != 0 ] && [ "$EUID" != 0 ]; then
	errorexit "${SCRIPTNAME} must be run as root."
fi

#
# Input arg checking
#

# There are three top level commands: --fsextend, --fsreduce, --cryptresize.
if [[ "$DO_FSEXTEND" -eq 0 && "$DO_FSREDUCE" -eq 0 && "$DO_CRYPTRESIZE" -eq 0 ]]; then
	errorexit "Missing --fsextend|--fsreduce|--cryptresize."
fi

if [[ "$DO_FSEXTEND" -eq 1 || "$DO_FSREDUCE" -eq 1 ]]; then
	case "$FSTYPE" in
	  ext[234]) ;;
	  "xfs")    ;;
	  "btrfs")  ;;
	  *) errorexit "Cannot resize --fstype \"$FSTYPE\"."
	esac

	if [ -z "$LVPATH" ]; then
		errorexit "Missing required --lvpath."
	fi
fi

if [[ "$DO_CRYPTRESIZE" -eq 1 && -z "$CRYPTPATH" ]]; then
	errorexit "Missing required --cryptpath for --cryptresize."
fi

if [ "$DO_CRYPTRESIZE" -eq 1 ]; then
	DEVPATH=$CRYPTPATH
else
	DEVPATH=$LVPATH
fi

if [ -z "$DEVPATH" ]; then
	errorexit "Missing path to device."
fi

if [ ! -e "$DEVPATH" ]; then
	errorexit "Device does not exist \"$DEVPATH\"."
fi

if [[ "$DO_UNMOUNT" -eq 1 && -z "$MOUNTDIR" ]]; then
	errorexit "Missing required --mountdir for --unmount."
fi

if [[ "$DO_FSREDUCE" -eq 1 && "$FSTYPE" == "xfs" ]]; then
	errorexit "Cannot reduce xfs."
fi

if [[ "$DO_FSCK" -eq 1 && "$FSTYPE" == "xfs" ]]; then
	errorexit "Cannot use --fsck with xfs."
fi

if [ "$DO_MOUNT" -eq 1 ]; then
	TMPDIR=$(mktemp --suffix _lvresize_$$ -d -p /tmp)
	if [ ! -e "$TMPDIR" ]; then
		errorexit "Failed to create temp dir."
	fi
	# In case the script terminates without doing cleanup
	function finish {
		if [ "$TMP_MOUNT_DONE" -eq 1 ]; then
			logmsg "exit unmount ${TMPDIR}"
			umount "$TMPDIR"
			rmdir "$TMPDIR"
		fi
	}
	trap finish EXIT
fi

#
# Main program function:
# - the two main functions are fsextend and fsreduce.
# - one special case function is cryptresize.
#

if [ "$DO_FSEXTEND" -eq 1 ]; then
	fsextend
elif [ "$DO_FSREDUCE" -eq 1 ]; then
	fsreduce
elif [ "$DO_CRYPTRESIZE" -eq 1 ]; then
	cryptresize
fi
