summaryrefslogtreecommitdiff
blob: 4b67d8b9082266ac753ec7c28c880e63216af8a4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
#!/bin/sh
#    Copyright (C) 2011 Canonical Ltd.
#    Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
#
#    Authors: Scott Moser <smoser@canonical.com>
#             Juerg Haefliger <juerg.haefliger@hp.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, version 3 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

# the fudge factor. if within this many bytes dont bother
FUDGE=${GROWPART_FUDGE:-$((1024*1024))}
TEMP_D=""
RESTORE_FUNC=""
RESTORE_HUMAN=""
VERBOSITY=0
DISK=""
PART=""
PT_UPDATE=false
DRY_RUN=0

SFDISK_VERSION=""
SFDISK_2_26="22600"
MBR_BACKUP=""
GPT_BACKUP=""
_capture=""

error() {
	echo "$@" 1>&2
}

fail() {
	[ $# -eq 0 ] || echo "FAILED:" "$@"
	exit 2
}

nochange() {
	echo "NOCHANGE:" "$@"
	exit 1
}

changed() {
	echo "CHANGED:" "$@"
	exit 0
}

change() {
	echo "CHANGE:" "$@"
	exit 0
}

cleanup() {
	if [ -n "${RESTORE_FUNC}" ]; then
		error "***** WARNING: Resize failed, attempting to revert ******"
		if ${RESTORE_FUNC} ; then
			error "***** Appears to have gone OK ****"
		else
			error "***** FAILED! ******"
			if [ -n "${RESTORE_HUMAN}" -a -f "${RESTORE_HUMAN}" ]; then
				error "**** original table looked like: ****"
				cat "${RESTORE_HUMAN}" 1>&2
			else
				error "We seem to have not saved the partition table!"
			fi
		fi
	fi
	[ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
}

debug() {
	local level=${1}
	shift
	[ "${level}" -gt "${VERBOSITY}" ] && return
	if [ "${DEBUG_LOG}" ]; then
		echo "$@" >>"${DEBUG_LOG}"
	else
		error "$@"
	fi
}

debugcat() {
	local level="$1"
	shift;
	[ "${level}" -gt "$VERBOSITY" ] && return
	if [ "${DEBUG_LOG}" ]; then
		cat "$@" >>"${DEBUG_LOG}"
	else
		cat "$@" 1>&2
	fi
}

mktemp_d() {
	# just a mktemp -d that doens't need mktemp if its not there.
	_RET=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX" 2>/dev/null) &&
		return
	_RET=$(umask 077 && t="${TMPDIR:-/tmp}/${0##*/}.$$" &&
		mkdir "${t}" &&	echo "${t}")
	return
}

Usage() {
	cat <<EOF
${0##*/} disk partition
   rewrite partition table so that partition takes up all the space it can
   options:
    -h | --help       print Usage and exit
         --fudge F    if part could be resized, but change would be
                      less than 'F' bytes, do not resize (default: ${FUDGE})
    -N | --dry-run    only report what would be done, show new 'sfdisk -d'
    -v | --verbose    increase verbosity / debug
    -u | --update  R  update the the kernel partition table info after growing
                      this requires kernel support and 'partx --update'
                      R is one of:
                       - 'auto'  : [default] update partition if possible
                       - 'force' : try despite sanity checks (fail on failure)
                       - 'off'   : do not attempt
                       - 'on'    : fail if sanity checks indicate no support

   Example:
    - ${0##*/} /dev/sda 1
      Resize partition 1 on /dev/sda
EOF
}

bad_Usage() {
	Usage 1>&2
	error "$@"
	exit 2
}

sfdisk_restore_legacy() {
	sfdisk --no-reread "${DISK}" -I "${MBR_BACKUP}"
}

sfdisk_restore() {
	# files are named: sfdisk-<device>-<offset>.bak
	local f="" offset="" fails=0
	for f in "${MBR_BACKUP}"*.bak; do
		[ -f "$f" ] || continue
		offset=${f##*-}
		offset=${offset%.bak}
		[ "$offset" = "$f" ] && {
			error "WARN: confused by file $f";
			continue;
		}
		dd "if=$f" "of=${DISK}" seek=$(($offset)) bs=1 conv=notrunc ||
			{ error "WARN: failed restore from $f"; fails=$(($fails+1)); }
	done
	return $fails
}

sfdisk_worked_but_blkrrpart_failed() {
	local ret="$1" output="$2"
	# exit code found was just 1, but dont insist on that
	#[ $ret -eq 1 ] || return 1
	# Successfully wrote the new partition table
	grep -qi "Success.* wrote.* new.* partition" "$output" &&
		grep -qi "BLKRRPART: Device or resource busy" "$output"
	return
}

get_sfdisk_version() {
	# set SFDISK_VERSION to MAJOR*10000+MINOR*100+MICRO
	local out oifs="$IFS" ver=""
	[ -n "$SFDISK_VERSION" ] && return 0
	# expected output: sfdisk from util-linux 2.25.2
	out=$(sfdisk --version) ||
		{ error "failed to get sfdisk version"; return 1; }
	set -- $out
	ver=$4
	case "$ver" in
		[0-9]*.[0-9]*.[0-9]|[0-9].[0-9]*)
			IFS="."; set -- $ver; IFS="$oifs"
			SFDISK_VERSION=$(($1*10000+$2*100+${3:-0}))
			return 0;;
		*) error "unexpected output in sfdisk --version [$out]"
			return 1;;
	esac
}

resize_sfdisk() {
	local humanpt="${TEMP_D}/recovery"
	local mbr_backup="${TEMP_D}/orig.save"
	local restore_func=""
	local format="$1"

	local change_out=${TEMP_D}/change.out
	local dump_out=${TEMP_D}/dump.out
	local new_out=${TEMP_D}/new.out
	local dump_mod=${TEMP_D}/dump.mod
	local tmp="${TEMP_D}/tmp.out"
	local err="${TEMP_D}/err.out"
	local mbr_max_512="4294967296"

	local pt_start pt_size pt_end max_end new_size change_info dpart
	local sector_num sector_size disk_size tot out

	rqe sfd_list sfdisk --list --unit=S "$DISK" >"$tmp" ||
		fail "failed: sfdisk --list $DISK"
	if [ "${SFDISK_VERSION}" -lt ${SFDISK_2_26} ]; then
		# exected output contains: Units: sectors of 512 bytes, ...
		out=$(awk '$1 == "Units:" && $5 ~ /bytes/ { print $4 }' "$tmp") ||
			fail "failed to read sfdisk output"
		if [ -z "$out" ]; then
			error "WARN: sector size not found in sfdisk output, assuming 512"
			sector_size=512
		else
			sector_size="$out"
		fi
		local _w _cyl _w1 _heads _w2 sectors _w3 t s
		# show-size is in units of 1024 bytes (same as /proc/partitions)
		t=$(sfdisk --show-size "${DISK}") ||
			fail "failed: sfdisk --show-size $DISK"
		disk_size=$((t*1024))
		sector_num=$(($disk_size/$sector_size))
		msg="disk size '$disk_size' not evenly div by sector size '$sector_size'"
		[ "$((${disk_size}%${sector_size}))" -eq 0 ] ||
			error "WARN: $msg"
		restore_func=sfdisk_restore_legacy
	else
		# --list first line output:
		# Disk /dev/vda: 20 GiB, 21474836480 bytes, 41943040 sectors
		local _x
		read _x _x _x _x disk_size _x sector_num _x  < "$tmp"
		sector_size=$((disk_size/$sector_num))
		restore_func=sfdisk_restore
	fi

	debug 1 "$sector_num sectors of $sector_size. total size=${disk_size} bytes"
	[ $(($disk_size/512)) -gt $mbr_max_512 ] &&
		debug 1 "WARN: disk is larger than 2TB. additional space will go unused."

	rqe sfd_dump sfdisk --unit=S --dump "${DISK}" >"${dump_out}" ||
		fail "failed to dump sfdisk info for ${DISK}"
	RESTORE_HUMAN="$dump_out"

	{
		echo "## sfdisk --unit=S --dump ${DISK}"
		cat "${dump_out}"
	}  >"$humanpt"

	[ $? -eq 0 ] || fail "failed to save sfdisk -d output"
	RESTORE_HUMAN="$humanpt"

	debugcat 1 "$humanpt"

	sed -e 's/,//g; s/start=/start /; s/size=/size /' "${dump_out}" \
		>"${dump_mod}" ||
		fail "sed failed on dump output"

	dpart="${DISK}${PART}" # disk and partition number
	if [ -b "${DISK}p${PART}" -a "${DISK%[0-9]}" != "${DISK}" ]; then
		# for block devices that end in a number (/dev/nbd0)
		# the partition is "<name>p<partition_number>" (/dev/nbd0p1)
		dpart="${DISK}p${PART}"
	elif [ "${DISK#/dev/loop[0-9]}" != "${DISK}" ]; then
		# for /dev/loop devices, sfdisk output will be <name>p<number>
		# format also, even though there is not a device there.
		dpart="${DISK}p${PART}"
	fi

	pt_start=$(awk '$1 == pt { print $4 }' "pt=${dpart}" <"${dump_mod}") &&
		pt_size=$(awk '$1 == pt { print $6 }' "pt=${dpart}" <"${dump_mod}") &&
		[ -n "${pt_start}" -a -n "${pt_size}" ] &&
		pt_end=$((${pt_size}+${pt_start})) ||
		fail "failed to get start and end for ${dpart} in ${DISK}"

	# find the minimal starting location that is >= pt_end 
	max_end=$(awk '$3 == "start" { if($4 >= pt_end && $4 < min)
		{ min = $4 } } END { printf("%s\n",min); }' \
		min=${sector_num} pt_end=${pt_end} "${dump_mod}") &&
		[ -n "${max_end}" ] ||
		fail "failed to get max_end for partition ${PART}"

	mbr_max_sectors=$((mbr_max_512*$((sector_size/512))))
	if [ "$max_end" -gt "$mbr_max_sectors" ]; then
		max_end=$mbr_max_sectors
	fi

	if [ "$format" = "gpt" ]; then
		# sfdisk respects 'last-lba' in input, and complains about
		# partitions that go past that.  without it, it does the right thing.
		sed -i '/^last-lba:/d' "$dump_out" ||
			fail "failed to remove last-lba from output"
	fi

	local gpt_second_size="33"
	if [ "${max_end}" -gt "$((${sector_num}-${gpt_second_size}))" ]; then
		# if mbr allow subsequent conversion to gpt without shrinking the
		# partition.  safety net at cost of 33 sectors, seems reasonable.
		# if gpt, we can't write there anyway.
		debug 1 "padding ${gpt_second_size} sectors for gpt secondary header"
		max_end=$((${sector_num}-${gpt_second_size}))
	fi

	debug 1 "max_end=${max_end} tot=${sector_num} pt_end=${pt_end}" \
		"pt_start=${pt_start} pt_size=${pt_size}"
	[ $((${pt_end})) -eq ${max_end} ] &&
		nochange "partition ${PART} is size ${pt_size}. it cannot be grown"
	[ $((${pt_end}+(${FUDGE}/$sector_size))) -gt ${max_end} ] &&
		nochange "partition ${PART} could only be grown by" \
		"$((${max_end}-${pt_end})) [fudge=$((${FUDGE}/$sector_size))]"

	# now, change the size for this partition in ${dump_out} to be the
	# new size
	new_size=$((${max_end}-${pt_start}))
	sed "\|^\s*${dpart} |s/${pt_size},/${new_size},/" "${dump_out}" \
		>"${new_out}" ||
		fail "failed to change size in output"

	change_info="partition=${PART} start=${pt_start} old: size=${pt_size} end=${pt_end} new: size=${new_size},end=${max_end}"
	if [ ${DRY_RUN} -ne 0 ]; then
		echo "CHANGE: ${change_info}"
		{
			echo "# === old sfdisk -d ==="
			cat "${dump_out}"
			echo "# === new sfdisk -d ==="
			cat "${new_out}"
		} 1>&2
		exit 0
	fi

	MBR_BACKUP="${mbr_backup}"
	LANG=C sfdisk --no-reread "${DISK}" --force \
		-O "${mbr_backup}" <"${new_out}" >"${change_out}" 2>&1
	ret=$?
	[ $ret -eq 0 ] || RESTORE_FUNC="${restore_func}"

	if [ $ret -eq 0 ]; then
		:
	elif $PT_UPDATE &&
		sfdisk_worked_but_blkrrpart_failed "$ret" "${change_out}"; then
		# if the command failed, but it looks like only because
		# the device was busy and we have pt_update, then go on
		debug 1 "sfdisk failed, but likely only because of blkrrpart"
	else
		error "attempt to resize ${DISK} failed. sfdisk output below:"
		sed 's,^,| ,' "${change_out}" 1>&2
		fail "failed to resize"
	fi

	rq pt_update pt_update "$DISK" "$PART" ||
		fail "pt_resize failed"

	RESTORE_FUNC=""

	changed "${change_info}"

	# dump_out looks something like:
	## partition table of /tmp/out.img
	#unit: sectors
	#
	#/tmp/out.img1 : start=        1, size=    48194, Id=83
	#/tmp/out.img2 : start=    48195, size=   963900, Id=83
	#/tmp/out.img3 : start=  1012095, size=   305235, Id=82
	#/tmp/out.img4 : start=  1317330, size=   771120, Id= 5
	#/tmp/out.img5 : start=  1317331, size=   642599, Id=83
	#/tmp/out.img6 : start=  1959931, size=    48194, Id=83
	#/tmp/out.img7 : start=  2008126, size=    80324, Id=83
}

gpt_restore() {
	sgdisk -l "${GPT_BACKUP}" "${DISK}"
}

resize_sgdisk() {
	GPT_BACKUP="${TEMP_D}/pt.backup"

	local pt_info="${TEMP_D}/pt.info"
	local pt_pretend="${TEMP_D}/pt.pretend"
	local pt_data="${TEMP_D}/pt.data"
	local out="${TEMP_D}/out"

	local dev="disk=${DISK} partition=${PART}"

	local pt_start pt_end pt_size last pt_max code guid name new_size
	local old new change_info sector_size
	
	# Dump the original partition information and details to disk. This is
	# used in case something goes wrong and human interaction is required
	# to revert any changes.
	rqe sgd_info sgdisk "--info=${PART}" --print "${DISK}" >"${pt_info}" ||
		fail "${dev}: failed to dump original sgdisk info"
	RESTORE_HUMAN="${pt_info}"

	sector_size=$(awk '$0 ~ /^Logical sector size:.*bytes/ { print $4 }' \
		"$pt_info") && [ -n "$sector_size" ] || {
		sector_size=512
		error "WARN: did not find sector size, assuming 512"
	}

	debug 1 "$dev: original sgdisk info:"
	debugcat 1 "${pt_info}"

	# Pretend to move the backup GPT header to the end of the disk and dump
	# the resulting partition information. We use this info to determine if
	# we have to resize the partition.
	rqe sgd_pretend sgdisk --pretend --move-second-header \
		--print "${DISK}" >"${pt_pretend}" ||
		fail "${dev}: failed to dump pretend sgdisk info"

	debug 1 "$dev: pretend sgdisk info"
	debugcat 1 "${pt_pretend}"

	# Extract the partition data from the pretend dump
	awk 'found { print } ; $1 == "Number" { found = 1 }' \
		"${pt_pretend}" >"${pt_data}" ||
		fail "${dev}: failed to parse pretend sgdisk info"

	# Get the start and end sectors of the partition to be grown
	pt_start=$(awk '$1 == '"${PART}"' { print $2 }' "${pt_data}") &&
		[ -n "${pt_start}" ] ||
		fail "${dev}: failed to get start sector"
	pt_end=$(awk '$1 == '"${PART}"' { print $3 }' "${pt_data}") &&
		[ -n "${pt_end}" ] ||
		fail "${dev}: failed to get end sector"
	pt_size="$((${pt_end} - ${pt_start}))"

	# Get the last usable sector
	last=$(awk '/last usable sector is/ { print $NF }' \
		"${pt_pretend}") && [ -n "${last}" ] ||
		fail "${dev}: failed to get last usable sector"

	# Find the minimal start sector that is >= pt_end 
	pt_max=$(awk '{ if ($2 >= pt_end && $2 < min) { min = $2 } } END \
		{ print min }' min="${last}" pt_end="${pt_end}" \
		"${pt_data}") && [ -n "${pt_max}" ] ||
		fail "${dev}: failed to find max end sector"

	debug 1 "${dev}: pt_start=${pt_start} pt_end=${pt_end}" \
		"pt_size=${pt_size} pt_max=${pt_max} last=${last}"

	# Check if the partition can be grown
	[ "${pt_end}" -eq "${pt_max}" ] &&
		nochange "${dev}: size=${pt_size}, it cannot be grown"
	[ "$((${pt_end} + ${FUDGE}/${sector_size}))" -gt "${pt_max}" ] &&
		nochange "${dev}: could only be grown by" \
		"$((${pt_max} - ${pt_end})) [fudge=$((${FUDGE}/$sector_size))]"

	# The partition can be grown if we made it here. Get some more info
	# about it so we can do it properly.
	# FIXME: Do we care about the attribute flags?
	code=$(awk '/^Partition GUID code:/ { print $4 }' "${pt_info}")
	guid=$(awk '/^Partition unique GUID:/ { print $4 }' "${pt_info}")
	name=$(awk '/^Partition name:/ { gsub(/'"'"'/, "") ; \
		if (NF >= 3) print substr($0, index($0, $3)) }' "${pt_info}")
	[ -n "${code}" -a -n "${guid}" ] ||
		fail "${dev}: failed to parse sgdisk details"

	debug 1 "${dev}: code=${code} guid=${guid} name='${name}'"
	local wouldrun=""
	[ "$DRY_RUN" -ne 0 ] && wouldrun="would-run"

	# Calculate the new size of the partition
	new_size=$((${pt_max} - ${pt_start}))
	old="old: size=${pt_size},end=${pt_end}"
	new="new: size=${new_size},end=${pt_max}"
	change_info="${dev}: start=${pt_start} ${old} ${new}"
	
	# Backup the current partition table, we're about to modify it
	rq sgd_backup $wouldrun sgdisk "--backup=${GPT_BACKUP}" "${DISK}" ||
		fail "${dev}: failed to backup the partition table"

	# Modify the partition table. We do it all in one go (the order is
	# important!):
	#  - move the GPT backup header to the end of the disk
	#  - delete the partition
	#  - recreate the partition with the new size
	#  - set the partition code
	#  - set the partition GUID
	#  - set the partition name
	rq sgdisk_mod $wouldrun sgdisk --move-second-header "--delete=${PART}" \
		"--new=${PART}:${pt_start}:${pt_max}" \
		"--typecode=${PART}:${code}" \
		"--partition-guid=${PART}:${guid}" \
		"--change-name=${PART}:${name}" "${DISK}" &&
		rq pt_update $wouldrun pt_update "$DISK" "$PART" || {
		RESTORE_FUNC=gpt_restore
		fail "${dev}: failed to repartition"
	}

	# Dry run
	[ "${DRY_RUN}" -ne 0 ] && change "${change_info}"

	changed "${change_info}"
}

kver_to_num() {
	local kver="$1" maj="" min="" mic="0"
	kver=${kver%%-*}
	maj=${kver%%.*}
	min=${kver#${maj}.}
	min=${min%%.*}
	mic=${kver#${maj}.${min}.}
	[ "$kver" = "$mic" ] && mic=0
	_RET=$(($maj*1000*1000+$min*1000+$mic))
}

kver_cmp() {
	local op="$2" n1="" n2=""
	kver_to_num "$1"
	n1="$_RET"
	kver_to_num "$3"
	n2="$_RET"
	[ $n1 $op $n2 ]
}

rq() {
	# runquieterror(label, command)
	# gobble stderr of a command unless it errors
	local label="$1" ret="" efile=""
	efile="$TEMP_D/$label.err"
	shift;

	local rlabel="running"
	[ "$1" = "would-run" ] && rlabel="would-run" && shift

	local cmd="" x=""
	for x in "$@"; do
		[ "${x#* }" != "$x" -o "${x#* \"}" != "$x" ] && x="'$x'"
		cmd="$cmd $x"
	done
	cmd=${cmd# }

	debug 2 "$rlabel[$label][$_capture]" "$cmd"
	[ "$rlabel" = "would-run" ] && return 0

	if [ "${_capture}" = "erronly" ]; then
		"$@" 2>"$TEMP_D/$label.err"
		ret=$?
	else
		"$@" >"$TEMP_D/$label.err" 2>&1
		ret=$?
	fi
	if [ $ret -ne 0 ]; then
		error "failed [$label:$ret]" "$@"
 		cat "$efile" 1>&2
	fi
	return $ret
}

rqe() {
	local _capture="erronly"
	rq "$@"
}

verify_ptupdate() {
	local input="$1" found="" reason="" kver=""

	# we can always satisfy 'off'
	if [ "$input" = "off" ]; then
 		_RET="false";
		return 0;
	fi

	if command -v partx >/dev/null 2>&1; then
		local out="" ret=0
		out=$(partx --help 2>&1)
		ret=$?
		if [ $ret -eq 0 ]; then
			echo "$out" | grep -q -- --update || {
				reason="partx has no '--update' flag in usage."
				found="off"
			}
		else
			reason="'partx --help' returned $ret. assuming it is old."
			found="off"
		fi
	else
		reason="no 'partx' command"
		found="off"
	fi

	if [ -z "$found" ]; then
		if [ "$(uname)" != "Linux" ]; then
			reason="Kernel is not Linux per uname."
			found="off"
		fi
	fi

	if [ -z "$found" ]; then
		kver=$(uname -r) || debug 1 "uname -r failed!"

		if ! kver_cmp "${kver-0.0.0}" -ge 3.8.0; then
			reason="Kernel '$kver' < 3.8.0."
			found="off"
		fi
	fi

	if [ -z "$found" ]; then
		_RET="true"
		return 0
	fi

	case "$input" in
		on) error "$reason"; return 1;;
		auto)
			_RET="false";
			debug 1 "partition update disabled: $reason"
			return 0;;
		force)
			_RET="true"
			error "WARNING: ptupdate forced on even though: $reason"
			return 0;;
	esac
	error "unknown input '$input'";
	return 1;
}

pt_update() {
	local dev="$1" part="$2" update="${3:-$PT_UPDATE}"
	if ! $update; then
		return 0
	fi
	# partx only works on block devices (do not run on file)
	[ -b "$dev" ] || return 0
	partx --update "$part" "$dev"
}

has_cmd() {
	command -v "${1}" >/dev/null 2>&1
}

resize_sgdisk_gpt() {
	resize_sgdisk gpt
}

resize_sgdisk_dos() {
	fail "unable to resize dos label with sgdisk"
}

resize_sfdisk_gpt() {
	resize_sfdisk gpt
}

resize_sfdisk_dos() {
	resize_sfdisk dos
}

get_table_format() {
	local out="" disk="$1"
	if has_cmd blkid && out=$(blkid -o value -s PTTYPE "$disk") &&
		[ "$out" = "dos" -o "$out" = "gpt" ]; then
		_RET="$out"
		return
	fi
	_RET="dos"
	if [ ${SFDISK_VERSION} -lt ${SFDISK_2_26} ] &&
		out=$(sfdisk --id --force "$disk" 1 2>/dev/null); then
		if [ "$out" = "ee" ]; then
			_RET="gpt"
		else
			_RET="dos"
		fi
		return
	elif out=$(LANG=C sfdisk --list "$disk"); then
		out=$(echo "$out" | sed -e '/Disklabel type/!d' -e 's/.*: //')
		case "$out" in
			gpt|dos) _RET="$out";;
			*) error "WARN: unknown label $out";;
		esac
	fi
}

get_resizer() {
	local format="$1" user=${2:-"auto"}

	case "$user" in
		sgdisk) _RET="resize_sgdisk_$format"; return;;
		sfdisk) _RET="resize_sfdisk_$format"; return;;
		auto) :;;
		*) error "unexpected input: '$user'";;
	esac

	if [ "$format" = "dos" ]; then
		_RET="resize_sfdisk_dos"
		return 0
	fi

	if [ "${SFDISK_VERSION}" -ge ${SFDISK_2_26} ]; then
		_RET="resize_sfdisk_gpt"
	elif has_cmd sgdisk; then
		_RET="resize_sgdisk_$format"
	else
		error "no tools available to resize disk with '$format'"
		return 1
	fi
	return 0
}

pt_update="auto"
resizer=${GROWPART_RESIZER:-"auto"}
while [ $# -ne 0 ]; do
	cur=${1}
	next=${2}
	case "$cur" in
		-h|--help)
			Usage
			exit 0
			;;
		--fudge)
			FUDGE=${next}
			shift
			;;
		-N|--dry-run)
			DRY_RUN=1
			;;
		-u|--update|--update=*)
			if [ "${cur#--update=}" != "$cur" ]; then
				next="${cur#--update=}"
			else
				shift
			fi
			case "$next" in
				off|auto|force|on) pt_update=$next;;
				*) fail "unknown --update option: $next";;
			esac
			;;
		-v|--verbose)
			VERBOSITY=$(($VERBOSITY+1))
			;;
		--)
			shift
			break
			;;
		-*)
			fail "unknown option ${cur}"
			;;
		*)
			if [ -z "${DISK}" ]; then
				DISK=${cur}
			else
				[ -z "${PART}" ] || fail "confused by arg ${cur}"
				PART=${cur}
			fi
			;;
	esac
	shift
done

[ -n "${DISK}" ] || bad_Usage "must supply disk and partition-number"
[ -n "${PART}" ] || bad_Usage "must supply partition-number"

has_cmd "sfdisk" || fail "sfdisk not found"
get_sfdisk_version || fail

[ -e "${DISK}" ] || fail "${DISK}: does not exist"

[ "${PART#*[!0-9]}" = "${PART}" ] || fail "partition-number must be a number"

verify_ptupdate "$pt_update" || fail
PT_UPDATE=$_RET

debug 1 "update-partition set to $PT_UPDATE"

mktemp_d && TEMP_D="${_RET}" || fail "failed to make temp dir"
trap cleanup 0 # EXIT - some shells may not like 'EXIT' but are ok with 0

# get the ID of the first partition to determine if it's MBR or GPT
get_table_format "$DISK" || fail
format=$_RET
get_resizer "$format" "$resizer" ||
    fail "failed to get a resizer for id '$id'"
resizer=$_RET

debug 1 "resizing $PART on $DISK using $resizer"
"$resizer"

# vi: ts=4 noexpandtab