/
#!/usr/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
FLOCK_DISK_FD=""
RESIZE_RESULT=""

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

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

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

nochange() {
    RESIZE_RESULT="NOCHANGE"
    echo "NOCHANGE:" "$@"
    return 1
}

changed() {
    RESIZE_RESULT="CHANGED"
    echo "CHANGED:" "$@"
    return 0
}

change() {
    RESIZE_RESULT="CHANGE"
    echo "CHANGE:" "$@"
    return 0
}

cleanup() {
    if [ -n "${RESTORE_FUNC}" ]; then
        error "***** WARNING: Resize failed, attempting to revert ******"
        if ${RESTORE_FUNC} ; then
            error "***** Restore appears to have gone OK ****"
        else
            error "***** Restore 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
        unlock_disk_and_settle $DISK
    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
         --free-percent F  resize so that specified percentage F of the disk is
                           not used in total (not just by this partition). This
                           is useful for consumer SSD or SD cards where a small
                           percentage unallocated can improve device lifetime.
         --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

    - ${0##*/} --free-percent=10 /dev/sda 1
      Resize partition 1 on /dev/sda so that 10% of the disk is unallocated
EOF
}

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

lock_disk() {
    local disk="$1"
    # flock the target disk to protect against udev actions while modifying.
    # https://systemd.io/BLOCK_DEVICE_LOCKING/
    [ "${DRY_RUN}" = 0 ] || return

    # only lock block devices, files do not need a lock
    [ -b "${disk}" ] || return

    # The FD values are hard-coded per /bin/sh requirement for using exec
    # to open paths with specific fd values; man (1) sh on "Redirections"
    FLOCK_DISK_FD=9
    debug 1 "FLOCK: try exec open fd 9, on failure exec exits this program"
    exec 9<>$disk

    # Do not use --nonblock or --timeout as udev may be already processing
    # the disk and we must wait until it has released the disk to
    # proceed.  Failure to obtain exclusive lock is fatal to growpart.
    rq flock flock -x $FLOCK_DISK_FD ||
        fail "Error while obtaining exclusive lock on $DISK"
    debug 1 "FLOCK: $disk: obtained exclusive lock"
}

unlock_disk_and_settle() {
    # unlock_disk(disk, settle)
    local disk="$1"
    local settle=${2-"1"}
    # release the lock on a disk if locked.  When a disk is locked,
    # FLOCK_DISK_FD is set to the hard-coded value of 9.
    # After unlocking run udevadm settle (if installed) as the disk has
    # likely been changed.
    [ "${DRY_RUN}" = 0 ] || return
    [ -n "${FLOCK_DISK_FD}" ] || return

    debug 1 "FLOCK: ${disk}: releasing exclusive lock"
    exec 9>&-
    [ "${settle}" = 1 ] && has_cmd udevadm && udevadm settle
    FLOCK_DISK_FD=""
}

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
    if grep -qi "Success.* wrote.* new.* partition" "$output"; then
        grep -qi "BLKRRPART: Device or resource busy" "$output"
        return
    # The partition table has been altered.
    elif grep -qi "The.* part.* table.* has.* been.* altered" "$output"; then
        # Re-reading the partition table failed
        grep -qi "Re-reading.* partition.* table.* failed" "$output"
        return
    fi
    return $ret
}

get_sfdisk_version() {
    # set SFDISK_VERSION to MAJOR*10000+MINOR*100+MICRO
    local out oifs="$IFS" ver=""
    [ -n "$SFDISK_VERSION" ] && return 0
    [ -n "$SFDISK" ] || {
        SFDISK_VERSION=0
        return 0
    }
    # expected output: sfdisk from util-linux 2.25.2
    out=$(LANG=C 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
}

get_diskpart_path() {
    # get_diskpart_path(disk, part_number)
    # return the path to the partition device on disk
    #
    # Convert inputs disk and part into a path to a partition device path
    # handle both block devices or files.
    # e.g.  get_diskpart_path /dev/loop0 7 returns /dev/loop0p7
    local disk="$1"
    local part="$2"
    local dpart=""

    dpart="${disk}${part}" # disk and partition number
    if [ -b "$disk" ]; then
        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
    else
        case "$disk" in
            # sfdisk for files ending in digit to <disk>p<num>.
            *[0-9]) dpart="${disk}p${part}";;
        esac
    fi
    _RET="$dpart"
}

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
    local excess_sectors free_percent_sectors remaining_free_sectors

    LANG=C 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"

    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"

    get_diskpart_path $DISK $PART
    dpart="$_RET"

    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} - 1)) ||
        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}"
    # As sector numbering starts from 0 need to reduce value by 1.
    max_end=$((max_end - 1))

    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
    if [ "$format" = "dos" ]; then
        mbr_max_sectors=$((mbr_max_512*$((sector_size/512))))
        if [ "$max_end" -gt "$mbr_max_sectors" ]; then
            max_end=$mbr_max_sectors
        fi
        [ $(($disk_size/512)) -gt $mbr_max_512 ] &&
            debug 0 "WARNING: MBR/dos partitioned disk is larger than 2TB." \
                "Additional space will go unused."
    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 and 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} - 1))
    fi

    if [ -n "${free_percent}" ]; then
        free_percent_sectors=$((sector_num/100*free_percent))

        if [ "$format" = "dos" ]; then
            if [ $(($disk_size/512)) -ge $((mbr_max_512+free_percent_sectors)) ]; then
                # If MBR partitioned disk larger than 2TB and
                # remaining space over 2TB boundary is greater
                # than the requested overprovisioning sectors
                # then do not change max_end.
                debug 1 "WARNING: Additional unused space on MBR/dos partitioned disk" \
                    "is larger than requested percent of overprovisioning."
            elif [ $sector_num -gt $mbr_max_512 ]; then
                # If only some of the overprovisioning sectors
                # are over the 2TB boundary then reduce max_end
                # by the remaining number of overprovisioning
                # sectors.
                excess_sectors=$((sector_num-mbr_max_512))
                remaining_free_sectors=$((free_percent_sectors - excess_sectors))
                debug 1 "reserving ${remaining_free_sectors} sectors from MBR maximum for overprovisioning"
                max_end=$((max_end - remaining_free_sectors))
            else
                # Shrink max_end to keep X% of whole disk unused
                # (for overprovisioning).
                debug 1 "reserving ${free_percent_sectors} sectors (${free_percent}%) for overprovisioning"
                max_end=$((max_end-free_percent_sectors))
            fi

            if [ ${max_end} -lt ${pt_end} ]; then
                nochange "partition ${PART} could not be grown while leaving" \
                    "${free_percent}% (${free_percent_sectors} sectors) free on device"
                return
            fi
        else
            # Shrink max_end to keep X% of whole disk unused
            # (for overprovisioning).
            debug 1 "reserving ${free_percent_sectors} sectors (${free_percent}%) for overprovisioning"
            max_end=$((max_end-free_percent_sectors))

            if [ ${max_end} -lt ${pt_end} ]; then
                nochange "partition ${PART} could not be grown while leaving" \
                    "${free_percent}% (${free_percent_sectors} sectors) free on device"
                return
            fi
        fi
    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"
        return
    }
    [ $((${pt_end}+(${FUDGE}/$sector_size))) -gt ${max_end} ] && {
        nochange "partition ${PART} could only be grown by" \
            "$((${max_end}-${pt_end})) [fudge=$((${FUDGE}/$sector_size))]"
        return
    }

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

    change_info="partition=${PART} start=${pt_start}"
    change_info="${change_info} old: size=${pt_size} end=${pt_end}"
    change_info="${change_info} 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
        debug 1 "resize of ${DISK} returned 0."
        if [ $VERBOSITY -gt 2 ]; then
            sed 's,^,| ,' "${change_out}" 1>&2
        fi
    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}"
    return

    # 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}"

    # support reading old format (<1.0.2)
    #   Logical sector size: <VAL> bytes
    # and newer format >= 1.0.2
    #   Sector size (logical): <VAL> bytes
    #   Sector size (logical/physical): <VAL>/<PVAL> bytes
    sector_size=$(awk '
        $0 ~ /^Logical sector size:.*bytes/ { print $4; exit(0); }
        $0 ~ /^Sector size \(logical\):/ { print $4; exit(0); }
        $0 ~ /^Sector size \(logical\/physical\):/ {
            sub(/\/.*/, "", $4); print $4; exit(0); }' \
        "$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"
    # Start and end are inclusive, start 2048 end 2057 is length 10.
    pt_size="$((${pt_end} - ${pt_start} + 1))"

    # 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 maximal end sector that is >= pt_end
    pt_max=$(awk '{ if ($2 >= pt_end && $2 < min) { min = $2 } } END \
        { print min-1 }' 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"
        return
    }
    [ "$((${pt_end} + ${FUDGE}/${sector_size}))" -gt "${pt_max}" ] && {
        nochange "${dev}: could only be grown by" \
            "$((${pt_max} - ${pt_end})) [fudge=$((${FUDGE}/$sector_size))]"
        return
    }

    # 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} + 1))
    change_info="partition=${PART} start=${pt_start}"
    change_info="${change_info} old: size=${pt_size} end=${pt_end}"
    change_info="${change_info} new: size=${new_size} end=${pt_max}"

    # 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-1))" \
        "--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}"
        return
    }

    changed "${change_info}"
    return
}

kver_to_num() {
    local kver="$1" maj min mic

    # Canonicalize the kernel version
    kver=${kver%%[!0-9.]*}.0.0

    maj=${kver%%[!0-9]*}
    kver=${kver#*.}
    min=${kver%%[!0-9]*}
    kver=${kver#*.}
    mic=${kver%%[!0-9]*}
    _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"
    test $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 --nr "$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 && blkid --version | grep -q util-linux &&
        out=$(blkid -o value -s PTTYPE "$disk") &&
        [ "$out" = "dos" -o "$out" = "gpt" ]; then
        _RET="$out"
        return
    fi
    _RET="dos"
    if [ -z "$SFDISK" ]; then
        out=$(LANG=C sgdisk --print "$disk") || {
            error "Could not determine partition table format of $disk" \
                "with 'sgdisk --print $disk'"
            return 1
        }
        # looking for a message like:
        # Found invalid GPT and valid MBR; converting MBR to GPT format
        case "$out" in
            *\ valid\ MBR\ *) _RET="dos";;
            *) _RET="gpt";;
        esac
        return
    elif [ ${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 value '$user' for growpart resizer"; return 1;;
    esac

    if [ "$format" = "dos" ]; then
        [ -n "$SFDISK" ] || {
            error "sfdisk is required for resizing dos/MBR partition table."
            return 1
        }
        _RET="resize_sfdisk_dos"
        return 0
    fi

    if [ "${SFDISK_VERSION}" -ge ${SFDISK_V_WORKING_GPT} ]; then
        # sfdisk 2.26.2 works for resize but loses type (LP: #1474090)
        _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
}

maybe_lvm_resize() {
    local disk="$1" part="$2" partpath="" ret="" out="" wouldrun=""
    [ "$DRY_RUN" -ne 0 ] && wouldrun="would-run"

    has_cmd lvm || {
        debug 2 "No lvm command, cannot attempt lvm resize of disk '$disk' part '$part'"
        return 0
    }
    get_diskpart_path "$1" "$2" || {
        error "could not determine partition path for disk '$DISK' part '$part'"
        return 1
    }
    partpath="$_RET"

    # can't use rq or rqe here because of "not an lvm" exit code 5.
    set -- lvm pvs --nolocking --readonly -o pvname "$partpath"
    debug 2 "executing: $*"
    out=$("$@" 2>&1)
    ret=$?
    case "$ret" in
        5) debug 1 "$partpath is not an lvm pv"; return 0;;
        0) :;;
        *) error "failed to execute [$ret] '$*'"
            error "$out"
            return 1;;
    esac
    rq lvm_resize $wouldrun lvm pvresize "$partpath" || {
        error "Failed to resize lvm pv $partpath"
        return 1
    }
    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
            ;;
        --free-percent|--free-percent=*)
            if [ "${cur#--free-percent=}" != "$cur" ]; then
                next="${cur#--free-percent=}"
            else
                shift
            fi
            if [ "$next" -gt 0 ] 2>/dev/null &&
                [ "$next" -lt 100 ] 2>/dev/null; then
                free_percent=$next
            else
                fail "unknown/invalid --free-percent option: $next"
            fi
            ;;
        --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"

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

# cache the has_cmd return value.
has_cmd sfdisk && SFDISK=sfdisk || SFDISK=""
has_cmd sgdisk && SGDISK=sgdisk || SGDISK=""

[ -n "$SGDISK" -o -n "$SFDISK" ] ||
    fail "Did not have sfdisk or sgdisk in PATH."

get_sfdisk_version || fail

# If $DISK is a symlink, resolve it.
# This avoids problems due to varying partition device name formats
# (e.g. "1" for /dev/sda vs "-part1" for /dev/disk/by-id/name)
if [ -L "${DISK}" ]; then
    has_cmd readlink ||
        fail "${DISK} is a symlink, but 'readlink' command not available."
    real_disk=$(readlink -f "${DISK}") || fail "unable to resolve ${DISK}"
    debug 1 "${DISK} resolved to ${real_disk}"
    DISK=${real_disk}
fi

[ "${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 format '$format'"
resizer=$_RET

lock_disk $DISK
debug 1 "resizing $PART on $DISK using $resizer"
"$resizer"
ret=$?

unlock_disk_and_settle $DISK

if [ "$RESIZE_RESULT" = "CHANGED" -o "$RESIZE_RESULT" = "CHANGE" ]; then
    maybe_lvm_resize "$DISK" "$PART" || fail "lvm resize failed."
fi

exit $ret

# vi: ts=4 noexpandtab