Skip to content

Commit

Permalink
Allow snapshot creation / move logic to function
Browse files Browse the repository at this point in the history
Some times it's helpful to be able to create a snapshot from the
bootloader, outside of the OS. On the snapshot screen, there's now a
MOD+N option to create a (N)ew snapshot. It rides along the coat-tails
of most of the snapshot cloning/duplication logic, as that has checks
for everything we also care about.

The MOD+S handler in zfsbootmenu.sh has grown quite large, with the
creation of quite a few global variables. The bulk of this is now hidden
inside snapshot_dispatcher(), which can use local variables for all of
the space calculations.
  • Loading branch information
zdykstra committed Sep 14, 2021
1 parent d208103 commit 891e44a
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 73 deletions.
170 changes: 165 additions & 5 deletions 90zfsbootmenu/zfsbootmenu-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -571,13 +571,12 @@ draw_snapshots() {

sort_key="$( get_sort_key )"

header="$( header_wrap "[RETURN] duplicate" "[ESCAPE] back" "" \
"[CTRL+X] clone and promote" "[CTRL+C] clone only" "" \
"[CTRL+J] jump into chroot" "[CTRL+D] show diff" "" \
"[CTRL+L] view logs" "[CTRL+H] help" )"
header="$( header_wrap "[RETURN] duplicate" "[CTRL+C] clone only" "[CTRL+X] clone and promote" "" \
"[CTRL+N] create new snapshot" "[CTRL+J] jump into chroot" "[CTRL+D] show diff" "" \
"[CTRL+L] view logs" "[CTRL+H] help" "[ESCAPE] back" )"
context="Note: for diff viewer, use tab to select/deselect up to two items"

expects="--expect=alt-x,alt-c,alt-j,alt-o"
expects="--expect=alt-x,alt-c,alt-j,alt-o,alt-n"

if ! selected="$( zfs list -t snapshot -H -o name "${benv}" -S "${sort_key}" |
HELP_SECTION=snapshot-management ${FUZZYSEL} \
Expand Down Expand Up @@ -901,6 +900,167 @@ clone_snapshot() {
return 0
}

# arg1: filesystem name
# arg2: snapshot name
# prints: nothing
# returns: 0 on success

create_snapshot() {
local selected target pool

selected="${1}"
if [ -z "$selected" ]; then
zerror "selected is undefined"
return 1
fi
zdebug "selected: ${selected}"

target="${2}"
if [ -z "$target" ]; then
zerror "target is undefined"
return 1
fi
zdebug "target: ${target}"

pool="${selected%%/*}"
if ! set_rw_pool "${pool}" ; then
zerror "unable to set pool ${pool} read/write"
return 1
fi

load_key "${selected}"

zdebug "creating snapshot ${selected}@${target}"
if ! output="$( zfs snapshot "${selected}@${target}" )" ; then
zdebug "unable to create snapshot: ${output}"
return 1
fi

return 0
}

# arg1: selected snapshot
# arg2: subkey
# prints: snapshot/filesystem creation prompt
# returns: nothing

snapshot_dispatcher() {
local selected subkey
local parent_ds avail_space_exact be_size_exact leftover_space avail_space be_size
local prompt header check_base pre_populated user_input valid_name clone_target

selected="${1}"
if [ -z "$selected" ]; then
zerror "selected is undefined"
return 1
fi
zdebug "selected: ${selected}"

subkey="${2}"
if [ -z "$subkey" ]; then
zerror "subkey is undefined"
return 1
fi
zdebug "subkey: ${subkey}"

parent_ds="${selected%/*}"
parent_ds="${selected%@*}"

if [ -z "${parent_ds}" ]; then
zerror "unable to determine parent dataset for ${selected}"
return 1
fi
zdebug "parent_ds: ${parent_ds}"

# Do space calculations; bail early
case "${subkey}" in
"enter")
avail_space_exact="$( zfs list -p -H -o available "${parent_ds}" )"
be_size_exact="$( zfs list -p -H -o refer "${selected}" )"
leftover_space=$(( avail_space_exact - be_size_exact ))
if [ "${leftover_space}" -le 0 ]; then
avail_space="$( zfs list -H -o available "${parent_ds}" )"
be_size="$( zfs list -H -o refer "${selected}" )"
zerror "Insufficient space for duplication, ${parent_ds}' has ${avail_space} free but needs ${be_size}"
color=red delay=10 timed_prompt "Insufficient space for duplication" \
"'${parent_ds}' has ${avail_space} free but needs ${be_size}"
return 1
fi
;;
esac

# Set prompt, header, existing check prefix
case "${subkey}" in
"enter"|"mod-x"|"mod-c")
prompt="\nNew boot environment name (CTRL-C or leave blank to abort)"
header="$( center_string "${selected}" )"
check_base="${parent_ds}/"

pre_populated="${selected##*/}"
pre_populated="${pre_populated%%@*}_NEW"
;;
"mod-n")
prompt="\nNew snapshot name (CTRL-C or leave blank to abort)"
header="$( center_string "${selected%%@*}" )"
check_base="${selected%%@*}@"

pre_populated="$( printf "%(%Y-%m-%d-%H%M%S)T" )"
;;
esac

tput clear
tput cnorm
colorize green "${header}"

while true; do
echo -e "${prompt}"
user_input="$( /libexec/zfsbootmenu-input "${pre_populated}" )"

[ -n "${user_input}" ] || return

valid_name=$( echo "${user_input}" | tr -c -d 'a-zA-Z0-9-_.:' )
if [[ "${user_input}" != "${valid_name}" ]]; then
echo "${user_input} is invalid, ${valid_name} can be used"
pre_populated="${valid_name}"
elif zfs list -H -o name "${check_base}${user_input}" >/dev/null 2>&1; then
echo "${check_base}${user_input} already exists, please use another name"
pre_populated="${user_input}"
else
break
fi
done

[ -n "${user_input}" ] || return

# Print what we're doing for anything but snapshot creation
case "${subkey}" in
"enter"|"mod-x"|"mod-c")
clone_target="${parent_ds}/${user_input}"
be_size="$( zfs list -H -o refer "${selected}" )"
echo -e "\nCreating ${clone_target} from ${selected} (${be_size})"
;;
esac

# Finally, dispatch to one of the snapshot handler functions
case "${subkey}" in
"enter")
duplicate_snapshot "${selected}" "${clone_target}"
;;
"mod-x")
PROMOTE=1 clone_snapshot "${selected}" "${clone_target}"
;;
"mod-c")
clone_snapshot "${selected}" "${clone_target}"
;;
"mod-n")
create_snapshot "${selected%%@*}" "${user_input}"
# shellcheck disable=SC2034
BE_SELECTED=1
;;
esac
}


# arg1: ZFS filesystem
# arg2: default kernel path (omit to unset default)
# prints: nothing
Expand Down
73 changes: 5 additions & 68 deletions 90zfsbootmenu/zfsbootmenu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,6 @@ while true; do
selected_snap="${selected_snap%,*}"
zdebug "selected snapshot: ${selected_snap}"

# Parent of the selected dataset, must be nonempty
parent_ds="${selected_snap%/*}"
[ -n "$parent_ds" ] || continue

tput clear
tput cnorm

case "${subkey}" in
"mod-j")
zfs_chroot "${selected_snap}"
Expand All @@ -243,67 +236,11 @@ while true; do
BE_SELECTED=1
continue
;;
# Check available space early in the process
"enter")
avail_space_exact="$( zfs list -p -H -o available "${parent_ds}" )"
be_size_exact="$( zfs list -p -H -o refer "${selected_snap}" )"
leftover_space=$(( avail_space_exact - be_size_exact ))
if [ "${leftover_space}" -le 0 ]; then
avail_space="$( zfs list -H -o available "${parent_ds}" )"
be_size="$( zfs list -H -o refer "${selected_snap}" )"
zerror "Insufficient space for duplication, ${parent_ds}' has ${avail_space} free but needs ${be_size}"
color=red delay=10 timed_prompt "Insufficient space for duplication" \
"'${parent_ds}' has ${avail_space} free but needs ${be_size}"
continue
fi
*)
snapshot_dispatcher "${selected_snap}" "${subkey}"
continue
;;
esac

# Strip parent datasets
pre_populated="${selected_snap##*/}"
# Strip snapshot name and append NEW
pre_populated="${pre_populated%%@*}_NEW"

header="$( center_string "${selected_snap}" )"
colorize green "${header}"

while true; do
echo -e "\nNew boot environment name (leave blank to abort)"
new_be="$( /libexec/zfsbootmenu-input "${pre_populated}" )"

[ -n "${new_be}" ] || break

valid_name=$( echo "${new_be}" | tr -c -d 'a-zA-Z0-9-_.:' )
# If the entered name is invalid, set the prompt to the valid form of the name
if [[ "${new_be}" != "${valid_name}" ]]; then
echo "${new_be} is invalid, ${valid_name} can be used"
pre_populated="${valid_name}"
elif zfs list -H -o name "${parent_ds}/${new_be}" >/dev/null 2>&1; then
echo "${new_be} already exists, please use another name"
pre_populated="${new_be}"
else
break
fi
done

# Must have a nonempty name for the new BE
[ -n "${new_be}" ] || continue

clone_target="${parent_ds}/${new_be}"
be_size="$( zfs list -H -o refer "${selected_snap}" )"
echo -e "\nCreating ${clone_target} from ${selected_snap} (${be_size})"

case "${subkey}" in
"enter")
duplicate_snapshot "${selected_snap}" "${clone_target}"
;;
"mod-x")
PROMOTE=1 clone_snapshot "${selected_snap}" "${clone_target}"
;;
"mod-c")
clone_snapshot "${selected_snap}" "${clone_target}"
;;
esac
;;
"mod-r")
tput cnorm
Expand Down Expand Up @@ -346,9 +283,9 @@ while true; do
;;
"mod-j")
zfs_chroot "${selected_be}"
;;
;;
"mod-o")
change_sort
;;
;;
esac
done
6 changes: 6 additions & 0 deletions pod/online/snapshot-management.pod
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ This creates a boot environment from a snapshot with out modifying snapshot inhe

The operation will fail gracefully if the pool can not be set I<read/write>.

=item I<[MOD+N]> B<snapshot creation>

This creates a new snapshot of the currently selected boot environment. A new snapshot is useful if you need to repair a boot environment from a chroot, to allow for easy roll-back of the changes.

The operation will fail gracefully if the pool can not be set I<read/write>.

=item I<[MOD+D]> B<diff>

Compare the differences between snapshots and filesystems. A single snapshot can be selected and a diff will be generated between that and the current state of the filesystem. Two snapshots can be selected (and deselected) with the tab key and a diff will be generated between them.
Expand Down

0 comments on commit 891e44a

Please sign in to comment.