2 # Copyright (C) 2011 Canonical Ltd.
3 # Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
5 # Authors: Scott Moser <smoser@canonical.com>
6 # Juerg Haefliger <juerg.haefliger@hp.com>
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, version 3 of the License.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # the fudge factor. if within this many bytes dont bother
21 FUDGE=${GROWPART_FUDGE:-$((1024*1024))}
42 [ $# -eq 0 ] || echo "FAILED:" "$@"
62 if [ -n "${RESTORE_FUNC}" ]; then
63 error "***** WARNING: Resize failed, attempting to revert ******"
64 if ${RESTORE_FUNC} ; then
65 error "***** Appears to have gone OK ****"
67 error "***** FAILED! ******"
68 if [ -n "${RESTORE_HUMAN}" -a -f "${RESTORE_HUMAN}" ]; then
69 error "**** original table looked like: ****"
70 cat "${RESTORE_HUMAN}" 1>&2
72 error "We seem to have not saved the partition table!"
76 [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
82 [ "${level}" -gt "${VERBOSITY}" ] && return
83 if [ "${DEBUG_LOG}" ]; then
84 echo "$@" >>"${DEBUG_LOG}"
93 [ "${level}" -gt "$VERBOSITY" ] && return
94 if [ "${DEBUG_LOG}" ]; then
95 cat "$@" >>"${DEBUG_LOG}"
102 # just a mktemp -d that doens't need mktemp if its not there.
103 _RET=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX" 2>/dev/null) &&
105 _RET=$(umask 077 && t="${TMPDIR:-/tmp}/${0##*/}.$$" &&
106 mkdir "${t}" && echo "${t}")
112 ${0##*/} disk partition
113 rewrite partition table so that partition takes up all the space it can
115 -h | --help print Usage and exit
116 --fudge F if part could be resized, but change would be
117 less than 'F' bytes, do not resize (default: ${FUDGE})
118 -N | --dry-run only report what would be done, show new 'sfdisk -d'
119 -v | --verbose increase verbosity / debug
120 -u | --update R update the the kernel partition table info after growing
121 this requires kernel support and 'partx --update'
123 - 'auto' : [default] update partition if possible
124 - 'force' : try despite sanity checks (fail on failure)
125 - 'off' : do not attempt
126 - 'on' : fail if sanity checks indicate no support
129 - ${0##*/} /dev/sda 1
130 Resize partition 1 on /dev/sda
140 sfdisk_restore_legacy() {
141 sfdisk --no-reread "${DISK}" -I "${MBR_BACKUP}"
145 # files are named: sfdisk-<device>-<offset>.bak
146 local f="" offset="" fails=0
147 for f in "${MBR_BACKUP}"*.bak; do
148 [ -f "$f" ] || continue
150 offset=${offset%.bak}
151 [ "$offset" = "$f" ] && {
152 error "WARN: confused by file $f";
155 dd "if=$f" "of=${DISK}" seek=$(($offset)) bs=1 conv=notrunc ||
156 { error "WARN: failed restore from $f"; fails=$(($fails+1)); }
161 sfdisk_worked_but_blkrrpart_failed() {
162 local ret="$1" output="$2"
163 # exit code found was just 1, but dont insist on that
164 #[ $ret -eq 1 ] || return 1
165 # Successfully wrote the new partition table
166 grep -qi "Success.* wrote.* new.* partition" "$output" &&
167 grep -qi "BLKRRPART: Device or resource busy" "$output"
171 get_sfdisk_version() {
172 # set SFDISK_VERSION to MAJOR*10000+MINOR*100+MICRO
173 local out oifs="$IFS" ver=""
174 [ -n "$SFDISK_VERSION" ] && return 0
175 # expected output: sfdisk from util-linux 2.25.2
176 out=$(sfdisk --version) ||
177 { error "failed to get sfdisk version"; return 1; }
181 [0-9]*.[0-9]*.[0-9]|[0-9].[0-9]*)
182 IFS="."; set -- $ver; IFS="$oifs"
183 SFDISK_VERSION=$(($1*10000+$2*100+${3:-0}))
185 *) error "unexpected output in sfdisk --version [$out]"
191 local humanpt="${TEMP_D}/recovery"
192 local mbr_backup="${TEMP_D}/orig.save"
193 local restore_func=""
196 local change_out=${TEMP_D}/change.out
197 local dump_out=${TEMP_D}/dump.out
198 local new_out=${TEMP_D}/new.out
199 local dump_mod=${TEMP_D}/dump.mod
200 local tmp="${TEMP_D}/tmp.out"
201 local err="${TEMP_D}/err.out"
202 local mbr_max_512="4294967296"
204 local pt_start pt_size pt_end max_end new_size change_info dpart
205 local sector_num sector_size disk_size tot out
207 rqe sfd_list sfdisk --list --unit=S "$DISK" >"$tmp" ||
208 fail "failed: sfdisk --list $DISK"
209 if [ "${SFDISK_VERSION}" -lt ${SFDISK_2_26} ]; then
210 # exected output contains: Units: sectors of 512 bytes, ...
211 out=$(awk '$1 == "Units:" && $5 ~ /bytes/ { print $4 }' "$tmp") ||
212 fail "failed to read sfdisk output"
213 if [ -z "$out" ]; then
214 error "WARN: sector size not found in sfdisk output, assuming 512"
219 local _w _cyl _w1 _heads _w2 sectors _w3 t s
220 # show-size is in units of 1024 bytes (same as /proc/partitions)
221 t=$(sfdisk --show-size "${DISK}") ||
222 fail "failed: sfdisk --show-size $DISK"
223 disk_size=$((t*1024))
224 sector_num=$(($disk_size/$sector_size))
225 msg="disk size '$disk_size' not evenly div by sector size '$sector_size'"
226 [ "$((${disk_size}%${sector_size}))" -eq 0 ] ||
228 restore_func=sfdisk_restore_legacy
230 # --list first line output:
231 # Disk /dev/vda: 20 GiB, 21474836480 bytes, 41943040 sectors
233 read _x _x _x _x disk_size _x sector_num _x < "$tmp"
234 sector_size=$((disk_size/$sector_num))
235 restore_func=sfdisk_restore
238 debug 1 "$sector_num sectors of $sector_size. total size=${disk_size} bytes"
239 [ $(($disk_size/512)) -gt $mbr_max_512 ] &&
240 debug 1 "WARN: disk is larger than 2TB. additional space will go unused."
242 rqe sfd_dump sfdisk --unit=S --dump "${DISK}" >"${dump_out}" ||
243 fail "failed to dump sfdisk info for ${DISK}"
244 RESTORE_HUMAN="$dump_out"
247 echo "## sfdisk --unit=S --dump ${DISK}"
251 [ $? -eq 0 ] || fail "failed to save sfdisk -d output"
252 RESTORE_HUMAN="$humanpt"
254 debugcat 1 "$humanpt"
256 sed -e 's/,//g; s/start=/start /; s/size=/size /' "${dump_out}" \
258 fail "sed failed on dump output"
260 dpart="${DISK}${PART}" # disk and partition number
261 if [ -b "${DISK}p${PART}" -a "${DISK%[0-9]}" != "${DISK}" ]; then
262 # for block devices that end in a number (/dev/nbd0)
263 # the partition is "<name>p<partition_number>" (/dev/nbd0p1)
264 dpart="${DISK}p${PART}"
265 elif [ "${DISK#/dev/loop[0-9]}" != "${DISK}" ]; then
266 # for /dev/loop devices, sfdisk output will be <name>p<number>
267 # format also, even though there is not a device there.
268 dpart="${DISK}p${PART}"
271 pt_start=$(awk '$1 == pt { print $4 }' "pt=${dpart}" <"${dump_mod}") &&
272 pt_size=$(awk '$1 == pt { print $6 }' "pt=${dpart}" <"${dump_mod}") &&
273 [ -n "${pt_start}" -a -n "${pt_size}" ] &&
274 pt_end=$((${pt_size}+${pt_start})) ||
275 fail "failed to get start and end for ${dpart} in ${DISK}"
277 # find the minimal starting location that is >= pt_end
278 max_end=$(awk '$3 == "start" { if($4 >= pt_end && $4 < min)
279 { min = $4 } } END { printf("%s\n",min); }' \
280 min=${sector_num} pt_end=${pt_end} "${dump_mod}") &&
281 [ -n "${max_end}" ] ||
282 fail "failed to get max_end for partition ${PART}"
284 mbr_max_sectors=$((mbr_max_512*$((sector_size/512))))
285 if [ "$max_end" -gt "$mbr_max_sectors" ]; then
286 max_end=$mbr_max_sectors
289 if [ "$format" = "gpt" ]; then
290 # sfdisk respects 'last-lba' in input, and complains about
291 # partitions that go past that. without it, it does the right thing.
292 sed -i '/^last-lba:/d' "$dump_out" ||
293 fail "failed to remove last-lba from output"
296 local gpt_second_size="33"
297 if [ "${max_end}" -gt "$((${sector_num}-${gpt_second_size}))" ]; then
298 # if mbr allow subsequent conversion to gpt without shrinking the
299 # partition. safety net at cost of 33 sectors, seems reasonable.
300 # if gpt, we can't write there anyway.
301 debug 1 "padding ${gpt_second_size} sectors for gpt secondary header"
302 max_end=$((${sector_num}-${gpt_second_size}))
305 debug 1 "max_end=${max_end} tot=${sector_num} pt_end=${pt_end}" \
306 "pt_start=${pt_start} pt_size=${pt_size}"
307 [ $((${pt_end})) -eq ${max_end} ] &&
308 nochange "partition ${PART} is size ${pt_size}. it cannot be grown"
309 [ $((${pt_end}+(${FUDGE}/$sector_size))) -gt ${max_end} ] &&
310 nochange "partition ${PART} could only be grown by" \
311 "$((${max_end}-${pt_end})) [fudge=$((${FUDGE}/$sector_size))]"
313 # now, change the size for this partition in ${dump_out} to be the
315 new_size=$((${max_end}-${pt_start}))
316 sed "\|^\s*${dpart} |s/${pt_size},/${new_size},/" "${dump_out}" \
318 fail "failed to change size in output"
320 change_info="partition=${PART} start=${pt_start} old: size=${pt_size} end=${pt_end} new: size=${new_size},end=${max_end}"
321 if [ ${DRY_RUN} -ne 0 ]; then
322 echo "CHANGE: ${change_info}"
324 echo "# === old sfdisk -d ==="
326 echo "# === new sfdisk -d ==="
332 MBR_BACKUP="${mbr_backup}"
333 LANG=C sfdisk --no-reread "${DISK}" --force \
334 -O "${mbr_backup}" <"${new_out}" >"${change_out}" 2>&1
336 [ $ret -eq 0 ] || RESTORE_FUNC="${restore_func}"
338 if [ $ret -eq 0 ]; then
341 sfdisk_worked_but_blkrrpart_failed "$ret" "${change_out}"; then
342 # if the command failed, but it looks like only because
343 # the device was busy and we have pt_update, then go on
344 debug 1 "sfdisk failed, but likely only because of blkrrpart"
346 error "attempt to resize ${DISK} failed. sfdisk output below:"
347 sed 's,^,| ,' "${change_out}" 1>&2
348 fail "failed to resize"
351 rq pt_update pt_update "$DISK" "$PART" ||
352 fail "pt_resize failed"
356 changed "${change_info}"
358 # dump_out looks something like:
359 ## partition table of /tmp/out.img
362 #/tmp/out.img1 : start= 1, size= 48194, Id=83
363 #/tmp/out.img2 : start= 48195, size= 963900, Id=83
364 #/tmp/out.img3 : start= 1012095, size= 305235, Id=82
365 #/tmp/out.img4 : start= 1317330, size= 771120, Id= 5
366 #/tmp/out.img5 : start= 1317331, size= 642599, Id=83
367 #/tmp/out.img6 : start= 1959931, size= 48194, Id=83
368 #/tmp/out.img7 : start= 2008126, size= 80324, Id=83
372 sgdisk -l "${GPT_BACKUP}" "${DISK}"
376 GPT_BACKUP="${TEMP_D}/pt.backup"
378 local pt_info="${TEMP_D}/pt.info"
379 local pt_pretend="${TEMP_D}/pt.pretend"
380 local pt_data="${TEMP_D}/pt.data"
381 local out="${TEMP_D}/out"
383 local dev="disk=${DISK} partition=${PART}"
385 local pt_start pt_end pt_size last pt_max code guid name new_size
386 local old new change_info sector_size
388 # Dump the original partition information and details to disk. This is
389 # used in case something goes wrong and human interaction is required
390 # to revert any changes.
391 rqe sgd_info sgdisk "--info=${PART}" --print "${DISK}" >"${pt_info}" ||
392 fail "${dev}: failed to dump original sgdisk info"
393 RESTORE_HUMAN="${pt_info}"
395 sector_size=$(awk '$0 ~ /^Logical sector size:.*bytes/ { print $4 }' \
396 "$pt_info") && [ -n "$sector_size" ] || {
398 error "WARN: did not find sector size, assuming 512"
401 debug 1 "$dev: original sgdisk info:"
402 debugcat 1 "${pt_info}"
404 # Pretend to move the backup GPT header to the end of the disk and dump
405 # the resulting partition information. We use this info to determine if
406 # we have to resize the partition.
407 rqe sgd_pretend sgdisk --pretend --move-second-header \
408 --print "${DISK}" >"${pt_pretend}" ||
409 fail "${dev}: failed to dump pretend sgdisk info"
411 debug 1 "$dev: pretend sgdisk info"
412 debugcat 1 "${pt_pretend}"
414 # Extract the partition data from the pretend dump
415 awk 'found { print } ; $1 == "Number" { found = 1 }' \
416 "${pt_pretend}" >"${pt_data}" ||
417 fail "${dev}: failed to parse pretend sgdisk info"
419 # Get the start and end sectors of the partition to be grown
420 pt_start=$(awk '$1 == '"${PART}"' { print $2 }' "${pt_data}") &&
421 [ -n "${pt_start}" ] ||
422 fail "${dev}: failed to get start sector"
423 pt_end=$(awk '$1 == '"${PART}"' { print $3 }' "${pt_data}") &&
424 [ -n "${pt_end}" ] ||
425 fail "${dev}: failed to get end sector"
426 pt_size="$((${pt_end} - ${pt_start}))"
428 # Get the last usable sector
429 last=$(awk '/last usable sector is/ { print $NF }' \
430 "${pt_pretend}") && [ -n "${last}" ] ||
431 fail "${dev}: failed to get last usable sector"
433 # Find the minimal start sector that is >= pt_end
434 pt_max=$(awk '{ if ($2 >= pt_end && $2 < min) { min = $2 } } END \
435 { print min }' min="${last}" pt_end="${pt_end}" \
436 "${pt_data}") && [ -n "${pt_max}" ] ||
437 fail "${dev}: failed to find max end sector"
439 debug 1 "${dev}: pt_start=${pt_start} pt_end=${pt_end}" \
440 "pt_size=${pt_size} pt_max=${pt_max} last=${last}"
442 # Check if the partition can be grown
443 [ "${pt_end}" -eq "${pt_max}" ] &&
444 nochange "${dev}: size=${pt_size}, it cannot be grown"
445 [ "$((${pt_end} + ${FUDGE}/${sector_size}))" -gt "${pt_max}" ] &&
446 nochange "${dev}: could only be grown by" \
447 "$((${pt_max} - ${pt_end})) [fudge=$((${FUDGE}/$sector_size))]"
449 # The partition can be grown if we made it here. Get some more info
450 # about it so we can do it properly.
451 # FIXME: Do we care about the attribute flags?
452 code=$(awk '/^Partition GUID code:/ { print $4 }' "${pt_info}")
453 guid=$(awk '/^Partition unique GUID:/ { print $4 }' "${pt_info}")
454 name=$(awk '/^Partition name:/ { gsub(/'"'"'/, "") ; \
455 if (NF >= 3) print substr($0, index($0, $3)) }' "${pt_info}")
456 [ -n "${code}" -a -n "${guid}" ] ||
457 fail "${dev}: failed to parse sgdisk details"
459 debug 1 "${dev}: code=${code} guid=${guid} name='${name}'"
461 [ "$DRY_RUN" -ne 0 ] && wouldrun="would-run"
463 # Calculate the new size of the partition
464 new_size=$((${pt_max} - ${pt_start}))
465 old="old: size=${pt_size},end=${pt_end}"
466 new="new: size=${new_size},end=${pt_max}"
467 change_info="${dev}: start=${pt_start} ${old} ${new}"
469 # Backup the current partition table, we're about to modify it
470 rq sgd_backup $wouldrun sgdisk "--backup=${GPT_BACKUP}" "${DISK}" ||
471 fail "${dev}: failed to backup the partition table"
473 # Modify the partition table. We do it all in one go (the order is
475 # - move the GPT backup header to the end of the disk
476 # - delete the partition
477 # - recreate the partition with the new size
478 # - set the partition code
479 # - set the partition GUID
480 # - set the partition name
481 rq sgdisk_mod $wouldrun sgdisk --move-second-header "--delete=${PART}" \
482 "--new=${PART}:${pt_start}:${pt_max}" \
483 "--typecode=${PART}:${code}" \
484 "--partition-guid=${PART}:${guid}" \
485 "--change-name=${PART}:${name}" "${DISK}" &&
486 rq pt_update $wouldrun pt_update "$DISK" "$PART" || {
487 RESTORE_FUNC=gpt_restore
488 fail "${dev}: failed to repartition"
492 [ "${DRY_RUN}" -ne 0 ] && change "${change_info}"
494 changed "${change_info}"
498 local kver="$1" maj="" min="" mic="0"
503 mic=${kver#${maj}.${min}.}
504 [ "$kver" = "$mic" ] && mic=0
505 _RET=$(($maj*1000*1000+$min*1000+$mic))
509 local op="$2" n1="" n2=""
518 # runquieterror(label, command)
519 # gobble stderr of a command unless it errors
520 local label="$1" ret="" efile=""
521 efile="$TEMP_D/$label.err"
524 local rlabel="running"
525 [ "$1" = "would-run" ] && rlabel="would-run" && shift
529 [ "${x#* }" != "$x" -o "${x#* \"}" != "$x" ] && x="'$x'"
534 debug 2 "$rlabel[$label][$_capture]" "$cmd"
535 [ "$rlabel" = "would-run" ] && return 0
537 if [ "${_capture}" = "erronly" ]; then
538 "$@" 2>"$TEMP_D/$label.err"
541 "$@" >"$TEMP_D/$label.err" 2>&1
544 if [ $ret -ne 0 ]; then
545 error "failed [$label:$ret]" "$@"
552 local _capture="erronly"
557 local input="$1" found="" reason="" kver=""
559 # we can always satisfy 'off'
560 if [ "$input" = "off" ]; then
565 if command -v partx >/dev/null 2>&1; then
567 out=$(partx --help 2>&1)
569 if [ $ret -eq 0 ]; then
570 echo "$out" | grep -q -- --update || {
571 reason="partx has no '--update' flag in usage."
575 reason="'partx --help' returned $ret. assuming it is old."
579 reason="no 'partx' command"
583 if [ -z "$found" ]; then
584 if [ "$(uname)" != "Linux" ]; then
585 reason="Kernel is not Linux per uname."
590 if [ -z "$found" ]; then
591 kver=$(uname -r) || debug 1 "uname -r failed!"
593 if ! kver_cmp "${kver-0.0.0}" -ge 3.8.0; then
594 reason="Kernel '$kver' < 3.8.0."
599 if [ -z "$found" ]; then
605 on) error "$reason"; return 1;;
608 debug 1 "partition update disabled: $reason"
612 error "WARNING: ptupdate forced on even though: $reason"
615 error "unknown input '$input'";
620 local dev="$1" part="$2" update="${3:-$PT_UPDATE}"
624 # partx only works on block devices (do not run on file)
625 [ -b "$dev" ] || return 0
626 partx --update "$part" "$dev"
630 command -v "${1}" >/dev/null 2>&1
633 resize_sgdisk_gpt() {
637 resize_sgdisk_dos() {
638 fail "unable to resize dos label with sgdisk"
641 resize_sfdisk_gpt() {
645 resize_sfdisk_dos() {
650 local out="" disk="$1"
651 if has_cmd blkid && out=$(blkid -o value -s PTTYPE "$disk") &&
652 [ "$out" = "dos" -o "$out" = "gpt" ]; then
657 if [ ${SFDISK_VERSION} -lt ${SFDISK_2_26} ] &&
658 out=$(sfdisk --id --force "$disk" 1); then
659 if [ "$out" = "ee" ]; then
665 elif out=$(LANG=C sfdisk --list "$disk"); then
666 out=$(echo "$out" | sed -e '/Disklabel type/!d' -e 's/.*: //')
668 gpt|dos) _RET="$out";;
669 *) error "WARN: unknown label $out";;
675 local format="$1" user=${2:-"auto"}
678 sgdisk) _RET="resize_sgdisk_$format"; return;;
679 sfdisk) _RET="resize_sfdisk_$format"; return;;
681 *) error "unexpected input: '$user'";;
684 if [ "$format" = "dos" ]; then
685 _RET="resize_sfdisk_dos"
689 if [ "${SFDISK_VERSION}" -ge ${SFDISK_2_26} ]; then
690 _RET="resize_sfdisk_gpt"
691 elif has_cmd sgdisk; then
692 _RET="resize_sgdisk_$format"
694 error "no tools available to resize disk with '$format'"
701 resizer=${GROWPART_RESIZER:-"auto"}
702 while [ $# -ne 0 ]; do
717 -u|--update|--update=*)
718 if [ "${cur#--update=}" != "$cur" ]; then
719 next="${cur#--update=}"
724 off|auto|force|on) pt_update=$next;;
725 *) fail "unknown --update option: $next";;
729 VERBOSITY=$(($VERBOSITY+1))
736 fail "unknown option ${cur}"
739 if [ -z "${DISK}" ]; then
742 [ -z "${PART}" ] || fail "confused by arg ${cur}"
750 [ -n "${DISK}" ] || bad_Usage "must supply disk and partition-number"
751 [ -n "${PART}" ] || bad_Usage "must supply partition-number"
753 has_cmd "sfdisk" || fail "sfdisk not found"
754 get_sfdisk_version || fail
756 [ -e "${DISK}" ] || fail "${DISK}: does not exist"
758 [ "${PART#*[!0-9]}" = "${PART}" ] || fail "partition-number must be a number"
760 verify_ptupdate "$pt_update" || fail
763 debug 1 "update-partition set to $PT_UPDATE"
765 mktemp_d && TEMP_D="${_RET}" || fail "failed to make temp dir"
766 trap cleanup 0 # EXIT - some shells may not like 'EXIT' but are ok with 0
768 # get the ID of the first partition to determine if it's MBR or GPT
769 get_table_format "$DISK" || fail
771 get_resizer "$format" "$resizer" ||
772 fail "failed to get a resizer for id '$id'"
775 debug 1 "resizing $PART on $DISK using $resizer"
778 # vi: ts=4 noexpandtab