#!/bin/bash
#
# Copyright (C) 2022 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

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

logmsg() {
	echo "$1"
	logger "${SCRIPTNAME}: $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

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

	if [ "$DO_FSCK" -eq 1 ]; then
		logmsg "e2fsck ${DEVPATH}"
		e2fsck -f -p "$DEVPATH"
		if [ $? -eq 0 ]; then
			logmsg "e2fsck done"
		else
			logmsg "e2fsck failed"
			exit 1
		fi
	fi
	
	if [ "$DO_CRYPTRESIZE" -eq 1 ]; then
		logmsg "cryptsetup resize ${DEVPATH}"
		cryptsetup resize "$DEVPATH"
		if [ $? -eq 0 ]; then
			logmsg "cryptsetup done"
		else
			logmsg "cryptsetup failed"
			exit 1
		fi
	fi

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

	if [[ "$FSTYPE" == "ext"* ]]; then
		logmsg "resize2fs ${DEVPATH}"
		resize2fs "$DEVPATH"
		if [ $? -eq 0 ]; then
			logmsg "resize2fs done"
		else
			logmsg "resize2fs failed"
			RESIZEFS_FAILED=1
		fi
	elif [[ "$FSTYPE" == "xfs" ]]; then
		logmsg "xfs_growfs ${DEVPATH}"
		xfs_growfs "$DEVPATH"
		if [ $? -eq 0 ]; then
			logmsg "xfs_growfs done"
		else
			logmsg "xfs_growfs failed"
			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}"
		umount "$TMPDIR"
		if [ $? -eq 0 ]; then
			logmsg "cleanup unmount done"
			TMP_MOUNT_DONE=0
			rmdir "$TMPDIR"
		else
			logmsg "cleanup unmount 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}"
		mount -t "$FSTYPE" "$DEVPATH" "$MOUNTDIR"
		if [ $? -eq 0 ]; then
			logmsg "remount done"
		else
			logmsg "remount failed"
		fi
	fi

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

	exit 0
}

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

	if [ "$DO_FSCK" -eq 1 ]; then
		logmsg "e2fsck ${DEVPATH}"
		e2fsck -f -p "$DEVPATH"
		if [ $? -eq 0 ]; then
			logmsg "e2fsck done"
		else
			logmsg "e2fsck failed"
			exit 1
		fi
	fi
	
	if [ "$DO_MOUNT" -eq 1 ]; then
		logmsg "mount ${DEVPATH} ${TMPDIR}"
		mount -t "$FSTYPE" "$DEVPATH" "$TMPDIR"
		if [ $? -eq 0 ]; then
			logmsg "mount done"
			TMP_MOUNT_DONE=1
		else
			logmsg "mount failed"
			exit 1
		fi
	fi

	if [[ "$FSTYPE" == "ext"* ]]; then
		NEWSIZEKB=$(($NEWSIZEBYTES/1024))
		logmsg "resize2fs ${DEVPATH} ${NEWSIZEKB}k"
		resize2fs "$DEVPATH" "$NEWSIZEKB"k
		if [ $? -eq 0 ]; then
			logmsg "resize2fs done"
		else
			logmsg "resize2fs failed"
			# will exit after cleanup unmount
			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}"
		umount "$TMPDIR"
		if [ $? -eq 0 ]; then
			logmsg "cleanup unmount done"
			TMP_MOUNT_DONE=0
			rmdir "$TMPDIR"
		else
			logmsg "cleanup unmount failed"
			exit 1
		fi
	fi

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

	if [ "$DO_CRYPTRESIZE" -eq 1 ]; then
		NEWSIZESECTORS=$(($NEWSIZEBYTES/512))
		logmsg "cryptsetup resize ${NEWSIZESECTORS} sectors ${DEVPATH}"
		cryptsetup resize --size "$NEWSIZESECTORS" "$DEVPATH"
		if [ $? -eq 0 ]; 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}"
		mount -t "$FSTYPE" "$DEVPATH" "$MOUNTDIR"
		if [ $? -eq 0 ]; then
			logmsg "remount done"
		else
			logmsg "remount failed"
		fi
	fi

	exit 0
}

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

	exit 0
}

usage() {
	echo "${SCRIPTNAME}: helper script called by lvresize to resize file systems."
	echo ""
	echo "${SCRIPTNAME} --fsextend --fstype name --lvpath path"
	echo "    [ --mountdir path ]"
	echo "    [ --mount ]"
	echo "    [ --unmount ]"
	echo "    [ --remount ]"
	echo "    [ --fsck ]"
	echo "    [ --cryptresize ]"
	echo "    [ --cryptpath path ]"
	echo ""
	echo "${SCRIPTNAME} --fsreduce --fstype name --lvpath path"
	echo "    [ --newsizebytes num ]"
	echo "    [ --mountdir path ]"
	echo "    [ --mount ]"
	echo "    [ --unmount ]"
	echo "    [ --remount ]"
	echo "    [ --fsck ]"
	echo "    [ --cryptresize ]"
	echo "    [ --cryptpath path ]"
	echo ""
	echo "${SCRIPTNAME} --cryptresize --cryptpath path --newsizebytes num"
	echo ""
	echo "Options:"
	echo "    --fsextend"
	echo "        Extend the file system."
	echo "    --fsreduce"
	echo "        Reduce the file system."
	echo "    --fstype name"
	echo "        The type of file system (ext*, xfs)."
	echo "    --lvpath path"
	echo "        The path to the LV being resized."
	echo "    --mountdir path"
	echo "        The file system is currently mounted here."
	echo "    --mount"
	echo "        Mount the file system on a temporary directory before resizing."
	echo "    --unmount"
	echo "        Unmount the file system before resizing."
	echo "    --remount"
	echo "        Remount the file system after resizing if unmounted."
	echo "    --fsck"
	echo "        Run fsck on the file system before resizing (only with ext*)."
	echo "    --newsizebytes num"
	echo "        The new size of the file system."
	echo "    --cryptresize"
	echo "        Resize the crypt device between the LV and file system."
	echo "    --cryptpath path"
	echo "        The path to the crypt device."
	echo ""
}

#
# BEGIN SCRIPT
#
PATH="/sbin:/usr/sbin:/bin:/usr/sbin:$PATH"
SCRIPTNAME=$(basename "$0")

# 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

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

GETOPT="getopt"

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

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

#
# 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")    ;;
	  *) 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

