1#!/bin/bash
2#
3# SSD TRIM utility for live RAID1 mirrored ext4 drives.
4#
5# By Chris Caputo.  Adapted from wiper.sh (ver 2.6) by Mark Lord.
6
7VERSION=1.5
8
9# Copyright (C) 2010-2012 Chris Caputo.  All rights reserved.
10#
11# This program is free software; you can redistribute it and/or
12# modify it under the terms of the GNU General Public License Version 2,
13# as published by the Free Software Foundation.
14#
15# This program is distributed in the hope that it would be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program; if not, write to the Free Software Foundation,
22# Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23
24function usage_error(){
25	echo "Usage:"
26	echo " ${0##*/} [--verbose] [--commit] [--reserve=#megs] [--max-ranges=#ranges] <raid_dev> <fsdir>"
27	echo "Examples:"
28	echo " ${0##*/} --verbose --commit --reserve=100 --max-ranges=512 md0 /"
29	echo " ${0##*/} --verbose --verbose md1 /boot"
30	echo 
31	echo "Note: For best results, this script should be run on each ext4-based filesystem present on a RAID1 array."
32	echo
33	exit 1
34}
35
36echo "${0##*/}: TRIM utility for live RAID1 ext4 SATA SSDs, version $VERSION, by Chris Caputo, based on Mark Lord's wiper.sh."
37echo
38
39## Parameter parsing for the main script.
40##
41
42export verbose=0
43commit=""
44reservemegs=0
45max_ranges=0
46argc=$#
47raiddev=""
48fsdir=""
49while [ $argc -gt 0 ]; do
50	if [ "$1" = "--commit" ]; then
51		commit=yes
52	elif [ "$1" = "--verbose" ]; then
53		verbose=$((verbose + 1))
54	elif [[ "$1" =~ --reserve= ]]; then
55		reservemegs=${1##--reserve=}
56	elif [[ "$1" =~ --max-ranges= ]]; then
57		max_ranges=${1##--max-ranges=}
58	elif [ "$1" = "" ]; then
59		usage_error
60	else
61		if [ "$raiddev" = "" ]; then
62			raiddev=${1##*/}
63		elif [ "$fsdir" = "" ]; then
64			fsdir=$1
65		else
66			echo "$1: too many arguments, aborting."
67			exit 1
68		fi
69	fi
70	argc=$((argc - 1))
71	shift
72done
73[ "$raiddev" = "" ] && usage_error
74[ "$fsdir" = "" ] && usage_error
75
76## Check --reserve number.
77##
78
79isdigit ()    # Tests whether *entire string* is numerical.
80{             # In other words, tests for integer variable.
81	[ $# -eq 1 ] || return 1
82
83	case $1 in
84		*[!0-9]*|"") return 1;;
85		*) return 0;;
86	esac
87}
88
89if ! isdigit "$reservemegs" ; then
90	echo "'$reservemegs' is not numerical"
91	exit 1
92fi
93if ! isdigit "$max_ranges" ; then
94	echo "'$max_ranges' is not numerical"
95	exit 1
96fi
97
98if [ $reservemegs -eq 0 ]; then
99	echo "Reserve defaulting to 10 megabytes."
100	reservemegs=10
101fi
102reservekilos=$((reservemegs * 1024))
103
104## Find a required program, or else give a nicer error message than we'd otherwise see:
105##
106function find_prog(){
107	prog="$1"
108	if [ ! -x "$prog" ]; then
109		prog="${prog##*/}"
110		p=`type -f -P "$prog" 2>/dev/null`
111		if [ "$p" = "" ]; then
112			echo "$1: needed but not found, aborting."
113			exit 1
114		fi
115		prog="$p"
116		[ $verbose -gt 0 ] && echo "  --> using $prog instead of $1"
117	fi
118	echo "$prog"
119}
120
121## Ensure we have most of the necessary utilities available before trying to proceed:
122##
123hash -r  ## Refresh bash's cached PATH entries
124HDPARM=`find_prog /sbin/hdparm` || exit 1
125GAWK=`find_prog /usr/bin/gawk`  || exit 1
126GREP=`find_prog /bin/grep`      || exit 1
127ID=`find_prog /usr/bin/id`      || exit 1
128LS=`find_prog /bin/ls`          || exit 1
129DF=`find_prog /bin/df`          || exit 1
130RM=`find_prog /bin/rm`          || exit 1
131
132[ $verbose -gt 1 ] && HDPARM="$HDPARM --verbose"
133
134## I suppose this will confuse the three SELinux users out there:
135##
136if [ `$ID -u` -ne 0 ]; then
137	echo "Only the super-user can use this (try \"sudo $0\" instead), aborting."
138	exit 1
139fi
140
141## We need a very modern hdparm, for its --fallocate and --trim-sector-ranges-stdin flags:
142## Version 9.25 added automatic determination of safe max-size of TRIM commands.
143##
144HDPVER=`$HDPARM -V | $GAWK '{gsub("[^0-9.]","",$2); if ($2 > 0) print ($2 * 100); else print 0; exit(0)}'`
145if [ $HDPVER -lt 925 ]; then
146	echo "$HDPARM: version >= 9.25 is required, aborting."
147	exit 1
148fi
149
150## Check that this is a RAID1 device.
151##
152if ! $GREP raid1 /sys/block/$raiddev/md/level 1>/dev/null ; then
153	echo "$raiddev is not a RAID1 array."
154	exit 1
155fi
156
157## Get list of slave devices in the RAID1 mirror.
158##
159slaves=(`$LS /sys/block/$raiddev/slaves`)
160#slaves=(sda sdb sdc)
161#slaves=(md0)
162
163## Check for DEVTYPE disk and TRIM support on each slave.
164##
165index=0
166for slave in "${slaves[@]}"
167do
168	# Check that slave is of DEVTYPE disk.
169	if ! $GREP "DEVTYPE=disk" /sys/block/$slave/uevent 1>/dev/null ; then
170		echo "$slave is not a whole disk. This program only works with full-disk RAID1, not RAID1 partitions."
171		exit 1
172	fi
173
174	# Check that slave has TRIM support.  Exclude if not.
175	if ! $HDPARM -I /dev/$slave | $GREP -i '[ 	][*][ 	]*Data Set Management TRIM supported' &>/dev/null ; then
176		echo "$slave doesn't appear to support TRIM, per $HDPARM. Excluding."
177		unset slaves[index]
178	fi
179		
180	let "index = $index + 1"
181done
182if [ "${slaves[0]}" = "" ]; then
183	echo "No constituent of $raiddev array supports TRIM.  Aborting."
184	exit 1
185fi
186
187## Check that fsdir is on an ext4 volume.
188##
189lines=`$DF --type=ext4 $fsdir 2>/dev/null | $GREP -v ^Filesystem | wc -l`
190if [ $lines -ne 1 ]; then
191	echo "'$fsdir' does not appear to be on an ext4 filesystem.  Aborting."
192	exit 1
193fi
194
195## Check that fsdir is a directory.
196##
197if [ ! -d $fsdir ]; then
198	echo "'$fsdir' is not a directory.  Aborting."
199	exit 1
200fi
201
202## Check free space & calculate tmpfile size.
203##
204freesize=`$DF -P -B 1024 $fsdir | $GAWK '{r=$4}END{print r}'`
205if [ "$freesize" = "" ]; then
206	echo "'$fsdir' is unknown to '$DF'.  Aborting."
207	exit 1
208fi
209if [ $freesize -lt $reservekilos ]; then
210	echo "'$fsdir' available space of $freesize KB is less than the $reservekilos KB to be reserved for the TRIM operation.  Aborting." >&2
211	exit 1
212fi
213tmpsize=$((freesize - reservekilos)) 
214tmpfile="$fsdir/${0##*/}_TMPFILE.$$"
215
216## Clean up tmpfile (if any) and exit:
217##
218function do_cleanup(){
219	if [ -e $tmpfile ]; then
220		echo "Removing temporary file '$tmpfile'..."
221		$RM -f $tmpfile
222		if [ -e $tmpfile ]; then
223			echo "Failed to remove '$tmpfile'!!!"
224		fi
225	fi
226	[ $1 -eq 0 ] && echo "Done."
227	[ $1 -eq 0 ] || echo "Aborted." >&2
228	exit $1
229}
230
231## Prepare signal handling, in case we get interrupted while $tmpfile exists:
232##
233function do_abort(){
234	echo
235	do_cleanup 1
236}
237trap do_abort SIGTERM
238trap do_abort SIGQUIT
239trap do_abort SIGINT
240trap do_abort SIGHUP
241trap do_abort SIGPIPE
242
243## Do the fallocate.
244## This is where we finally discover whether the filesystem actually
245## supports --fallocate or not.  Some folks will be disappointed here.
246##
247## Note that --fallocate does not actually write any file data to fsdev,
248## but rather simply allocates formerly-free space to the tmpfile.
249##
250echo -n "Creating temporary file (${tmpsize} KB '$tmpfile') ... "
251if ! $HDPARM --fallocate "${tmpsize}" $tmpfile ; then
252	echo "This kernel may not support 'fallocate'.  Aborting."
253	exit 1
254fi
255echo
256
257## Verify that slaves and RAID1 mirror have same base LBA.  First add a test
258## string to the tmpfile.  "date" is used since it is ever changing.
259##
260TESTSTR=`date`
261echo "$TESTSTR" >> $tmpfile
262sync    # this is critical
263SECTOR_BYTES=`$HDPARM --fibmap $tmpfile | \
264		$GREP "byte sectors"    | \
265		$GAWK '{print $9}'`
266LAST_EXTENT_SECTOR_COUNT=`$HDPARM --fibmap $tmpfile | \
267				tail -1              | \
268				$GAWK '{print $4}'`
269LAST_EXTENT_LBA=`$HDPARM --fibmap $tmpfile | tail -1 | $GAWK '{print $2}'`
270
271## Verify the test string is in the extent read.
272if ! dd iflag=direct status=noxfer bs=$SECTOR_BYTES count=$LAST_EXTENT_SECTOR_COUNT skip=$LAST_EXTENT_LBA if=/dev/$raiddev 2>/dev/null | $GREP "$TESTSTR" &>/dev/null ; then
273	echo "Test string was not found in last extent of tmpfile, as it should have been.  Aborting."
274	do_cleanup 1
275fi
276
277## Now compare the mirror and the slaves to make sure they have the same data at the same LBA.
278##
279refchksum=`dd iflag=direct status=noxfer bs=$SECTOR_BYTES count=$LAST_EXTENT_SECTOR_COUNT skip=$LAST_EXTENT_LBA if=/dev/$raiddev 2>/dev/null | sha1sum`
280index=0
281for slave in "${slaves[@]}"
282do
283	chksum=`dd iflag=direct status=noxfer bs=$SECTOR_BYTES count=$LAST_EXTENT_SECTOR_COUNT skip=$LAST_EXTENT_LBA if=/dev/$slave 2>/dev/null | sha1sum`
284
285	if [ "$chksum" != "$refchksum" ]; then
286		echo "Direct I/O of last extent of tmpfile on $slave doesn't match that of $raiddev.  Excluding."
287		unset slaves[index]
288	fi
289		
290	let "index = $index + 1"
291done
292if [ "${slaves[0]}" = "" ]; then
293	echo "No constituent of $raiddev array has a matching checksum.  Aborting."
294	do_cleanup 1
295fi
296
297echo "TRIMable constituents of $raiddev: ${slaves[@]}"
298
299## If they specified "--commit" on the command line, then prompt for confirmation first:
300##
301if [ "$commit" = "yes" ]; then
302	echo "Beginning TRIM operations..."
303else
304	echo "This will be a DRY-RUN only.  Use --commit to do it for real."
305	echo "Simulating TRIM operations..."
306fi
307get_trimlist="$HDPARM --fibmap $tmpfile"
308[ $verbose -gt 0 ] && echo "get_trimlist=$get_trimlist"
309
310
311## Begin gawk program
312GAWKPROG='
313	function append_range (lba,count  ,this_count){
314		nsectors += count;
315		while (count > 0) {
316			this_count  = (count > 65535) ? 65535 : count
317			printf "%u:%u \n", lba, this_count
318			if (verbose > 1)
319				printf "%u:%u ", lba, this_count > "/dev/stderr"
320			lba        += this_count
321			count      -= this_count
322			nranges++;
323		}
324	}
325	{  ## Output from "hdparm --fibmap", in absolute sectors:
326		if (NF == 4 && $2 ~ "^[1-9][0-9]*$")
327		append_range($2,$4)
328		next
329	}
330	END {
331		if (verbose > 1)
332			printf "\n" > "/dev/stderr"
333		if (err == 0 && commit != "yes")
334			printf "(dry-run) trimming %u sectors from %u ranges\n", nsectors, nranges > "/dev/stderr"
335		exit err
336	}'
337## End gawk program
338
339## Run TRIM on each slave.  Batch as requested.
340sync
341index=0
342for slave in "${slaves[@]}"
343do
344	echo "TRIM beginning on $slave..."
345
346	if [ "$commit" = "yes" ]; then
347		TRIM="$HDPARM --please-destroy-my-drive \
348				--trim-sector-ranges-stdin /dev/$slave"
349	else
350		TRIM="$GAWK {}"
351	fi
352
353	## Different SSD's have a different maximum number of ranges they'll
354	## accept in a single TRIM command.
355	if [ $max_ranges -eq 0 ] ; then
356		model=`$HDPARM -I /dev/$slave | $GAWK '/Model Number/ { print $NF }'`
357		case "$model" in
358			SSDSA[12]*)  slave_max_range=512 ;; # Intel X18-M/X25-M
359			OCZ-VERTEX2) slave_max_range=64 ;; # OCZ Vertex2
360			*)           slave_max_range=65535
361		esac
362	else
363		slave_max_range=$max_ranges
364	fi
365	[ $verbose -gt 0 ] && echo "$slave: max-ranges = $slave_max_range"
366
367	$get_trimlist 2>/dev/null | $GAWK \
368		-v commit="$commit"       \
369		-v verbose="$verbose"     \
370		"$GAWKPROG" |             \
371		if true; then
372			i=0
373			while read range; do
374				(( i++ ))
375				if (( i <= $slave_max_range )); then
376					ranges=$ranges" "$range
377				else
378					[ $verbose -gt 0 ] && echo -e "Trim ranges:" $ranges
379					echo $ranges | $TRIM
380					ret=$?
381					if [ $ret -ne 0 ] ; then
382						do_cleanup $ret
383					fi
384					ranges=$range
385					i=1
386				fi
387			done
388			[ $verbose -gt 0 ] && echo -e "Trim ranges:" $ranges
389			echo $ranges | $TRIM
390			ret=$?
391			if [ $ret -ne 0 ] ; then
392				do_cleanup $ret
393			fi
394			ranges=""
395		fi
396				
397	ret=$?
398	if [ $ret -ne 0 ] ; then
399		echo "TRIM failed on $slave.  Aborting."
400		do_cleanup $ret
401	else
402		echo "TRIM finished successfully on $slave."
403	fi
404done
405
406do_cleanup 0
407
408