#!/bin/sh

# shrink ext[234]fs filesystem to minimimum size,
# rouded up to integral number of LVM PEs

set -e 
progname=$(basename $0)

[ "$#" -eq 1 ] || {
	2>&1 echo "usage: $progname filesystem"
	exit 1
}

# our presumed filesystem
fs="$1"

# some temporary files for capturing stdout/stderr, and related cleanup
outf=
errf=
trap '
	[ -n "$outf" ] && rm -f "$outf"
	[ -n "$errf" ] && rm -f "$outf"
' 0 1 2 3 15
outf=$(mktemp)
errf=$(mktemp)

# to shrink, it needs to not be mounted
> "$outf" 2> "$errf" umount "$fs" || {
	rc="$?"
	{
		[ "$rc" -eq 1 ] &&
		< "$errf" >> /dev/null 2>&1 fgrep -x "umount: $fs: not mounted" &&
		[ 1 -eq $(< "$errf" wc -l) ]
		# it wasn't mounted, ignore this
		unset rc
	} || {
		# some other error, report it and exit appropriately
		1>&2 cat "$errf"
		exit "$rc"
	}
}

# need it to not be mounted or open
lvchange -a n "$fs" # this will fail if it's still mounted or open

# need it to be active to shrink it
lvchange -a y "$fs" # this will fail if it's not active or made active

# sanity check our calculating abilities, and use a method that works, or fail
# a pebibyte should be larg enough for our testing (well over 32 bits,
# likely covers full 64 bit)
if \
	[ 1125899906842624 = \
	$(
		expr 1024 \* 1024 \* 1024 \* 1024 \* 1024
	) ]
	then
	calculate(){
		expr "$@"
	}
elif \
	[ 1125899906842624 = \
	$(
		echo '1024 * 1024 * 1024 * 1024 * 1024' |
		bc -l
	) ]
	then
	calculate(){
		while [ "$#" -ge 2 ]
		do
			x="$1"; y="$2"
			shift
			shift
			set -- "$x$y" "$@"
		done
		printf '%s\n' "$1" | bc -l
	}
elif \
	[ 1125899906842624 = \
	$(
		perl -e 'print(1024 * 1024 * 1024 * 1024 * 1024,"\n");'
	) ]
	then
	calculate(){
		perl -e 'print('"$*"',"\n");'
	}
else
	1>&2 echo "$progname: failed to find adequate calculation method"
	exit 1
fi

# our calculate may include non-integral decimal portion
# we only use non-negative numbers, so do a simple floor
floor_calculate(){
	calculate "$@" |
	sed -e 's/\..*$//;s/^$/0/'
}

get_fs_information(){
	# ext[234] filesystem information we're interested in
	2>> /dev/null dumpe2fs "$fs" |
	> "$outf" grep -E '^Block (count|size):[ 	]*[0-9][0-9]*[ 	]*$'

	# ext[234] filesystem block size (in bytes)
	fs_Block_size=$(
		< "$outf" sed -ne 's/^Block size:[ 	]*\([0-9][0-9]*\)[ 	]*$/\1/p'
	)
	[ -n "$fs_Block_size" ] || exit 1

	# ext[234] filesystem block count
	fs_Block_count=$(
		< "$outf" sed -ne 's/^Block count:[ 	]*\([0-9][0-9]*\)[ 	]*$/\1/p'
	)
	[ -n "$fs_Block_count" ] || exit 1
	fs_size=$(floor_calculate "$fs_Block_size" \* "$fs_Block_count")
}

get_fs_information

# LVM information we're interested in
set -- $(lvs --noheadings -o vg_extent_size,lv_size --units b "$fs")
vg_extent_size="$1"
lv_size="$2"
set --

# volume group PE size (in bytes)
vg_extent_size=$(
	expr X"$vg_extent_size" : X'\([0-9][0-9]*\)B$'
)
[ -n "$vg_extent_size" ]

# LV size (in bytes)
lv_size=$(
	expr X"$lv_size" : X'\([0-9][0-9]*\)B$'
)
[ -n "$vg_extent_size" ]

[ $(floor_calculate "$lv_size" / "$vg_extent_size") -gt 1 ] || {
	echo "$progname: cannot shrink as only one PE of VG is used by $fs"
	exit 0
}

# sanity test
[ "$fs_Block_size" -le "$lv_size" ] || {
	echo "$progname: sanity test: [ fs_Block_size=$fs_Block_size -le lv_size=$lv_size ] failed"
	exit 1
}

try_resize2fs(){
	unset minimum
	unset fsck
	[ "$#" -eq 1 ] || {
		1>&2 echo "$progname: internal error - bad argument count to try_resize2fs"
		exit 1
	}
	> "$outf" 2> "$errf" resize2fs "$fs" "$1" && return
	rc="$?"
	if [ "$rc" -eq 1 ]; then
		minimum=$(
			< "$errf" sed -ne '
				/^resize2fs: New size smaller than minimum ([0-9][0-9]*)$/{
					s/^resize2fs: New size smaller than minimum (\([0-9][0-9]*\))$/\1/p
					q
				}
			'
		)
		if [ -n "$minimum" ]; then
			return "$rc"
		else
			unset minimum
		fi

		< "$errf" >>/dev/null grep -Fx "Please run 'e2fsck -f $fs' first." && {
			fsck='e2fsck -f -y '"$fs"
			return "$rc"
		}

		1>&2 cat "$errf"
		exit "$rc"
	else
		1>&2 cat "$errf"
		exit "$rc"
	fi
}

fs_shrunk_shrink_lv(){
	case "$#" in
		1)
			lvreduce -f -l "$1" "$fs"
			exit
		;;
		0)
			get_fs_information
			new_le_needed=$(floor_calculate "$fs_size" / "$vg_extent_size")
			# that could be one LE short due to floor
			[ \
				$(floor_calculate "$new_le_needed" \* "$vg_extent_size") \
				-ge \
				"$fs_size" \
			] || {
				new_le_needed=$(floor_calculate "$new_le_needed" + 1)
			}
			# and retest
			[ \
				$(floor_calculate "$new_le_needed" \* "$vg_extent_size") \
				-ge \
				"$fs_size" \
			] || {
				1>&2 echo "$progname: failed to get expected size data: new_le_needed=$new_le_needed vg_extent_size=$vg_extent_size fs_size=$fs_size"
				exit 1
			}
			old_le_count=$(floor_calculate "$lv_size" / "$vg_extent_size")
			if [ "$new_le_needed" -lt "$old_le_count" ]; then
				lvreduce -f -l "$new_le_needed" "$fs"
				exit
			elif [ "$new_le_needed" -eq "$old_le_count" ]; then
				echo "$progname: cannot reduce LEs of $fs"
				exit 0
			else
				1>&2 echo "$progname: got unexpected values for new_le_needed=$new_le_needed and old_le_count=$old_le_count"
				exit 1
			fi
		;;
		*)
			1>&2 echo "$progname: internal error - bad argument count to fs_shrunk_shrink_lv"
			exit 1
		;;
	esac
}

# first we try what's likely too small
try_fs_Blocks=$(floor_calculate "$vg_extent_size" / "$fs_Block_size")
[ "$try_fs_Blocks" -ge 1 ] && try_fs_Blocks=1
[ $(floor_calculate "$try_fs_Blocks" \* "$fs_Block_size") -lt "$lv_size" ] || {
	echo "$progname: cannot shrink"
	exit 0
}
if try_resize2fs "$try_fs_Blocks"; then
	# egad, it worked?
	fs_shrunk_shrink_lv
	exit
else
	# if we're here, resizefs exited non-zero, let's see what happened
	rc="$?"
	if [ -n "$minimum" ]; then
		# we tried too small, let's see if there's a size that makes sense
		new_fs_size=$(floor_calculate "$minimum" \* "$fs_Block_size")
		new_le_needed=$(floor_calculate "$new_fs_size" / "$vg_extent_size")
		# that could be one LE short due to floor
		[ \
			$(floor_calculate "$new_le_needed" \* "$vg_extent_size") \
			-ge \
			"$new_fs_size" \
		] || {
			new_le_needed=$(floor_calculate "$new_le_needed" + 1)
		}
		# and retest
		[ \
			$(floor_calculate "$new_le_needed" \* "$vg_extent_size") \
			-ge \
			"$new_fs_size" \
		] || {
			1>&2 echo "$progname: failed to get expected size data: new_le_needed=$new_le_needed vg_extent_size=$vg_extent_size new_fs_size=$new_fs_size"
			exit 1
		}
		new_fs_size=$(floor_calculate "$new_le_needed" \* "$vg_extent_size")
		new_fs_Block_count=$(floor_calculate "$new_fs_size" / "$fs_Block_size")
		# that should be good size, let's sanity check it
		{
			[ "$new_fs_Block_count" -ge "$minimum" ] &&
			[ "$new_fs_size" -lt "$fs_size" ] &&
			[ "$new_le_needed" -lt  \
				$(floor_calculate "$lv_size" / "$vg_extent_size") \
			] 

		} || {
			echo "$progname: cannot shrink - didn't pass our precondition sizing checks"
			exit 0
		}
		if try_resize2fs "$new_fs_Block_count"; then
			fs_shrunk_shrink_lv "$new_le_needed"
		elif [ "$rc" -eq 1 ] && [ -n "$fsck" ]; then
			if $fsck; then
				if try_resize2fs "$new_fs_Block_count"; then
					fs_shrunk_shrink_lv "$new_le_needed"; exit
				else
					rc="$?"
					2>&1 cat "$errf"
					exit "$rc"
				fi
			else
				rc="$?"
				2>&1 echo "$progname: perhaps try again, but aborting shrink attempt as we got non-zero return ($rc) from: $fsck"
				exit "$rc"
			fi
		else
			rc="$?"
			2>&1 cat "$errf"
			exit "$rc"
		fi
	elif [ -n "$fsck" ]; then
		if $fsck; then
			if try_resize2fs "$try_fs_Blocks"; then
				fs_shrunk_shrink_lv; exit
			else
				rc="$?"
				2>&1 cat "$errf"
				exit "$rc"
			fi
		else
			rc="$?"
			2>&1 echo "$progname: perhaps try again, but aborting shrink attempt as we got non-zero return ($rc) from: $fsck"
			exit "$rc"
		fi
	else
		1>&2 cat "$errf"
		exit "$rc"
	fi
fi
