Files
grub-btrfs/41_snapshots-btrfs
Kriss d68fdf5fff Adds a new config variable "GRUB_BTRFS_ROOTFLAGS" (#133)
Script:

Improves the rootflags option in the kernel command line:
- Remove "rw" flag.
- Parse fstab mountoptions and add them to rootflags:
Adds functionality to read in  snapshot specific mount flags
from the respective fstab and uses them in combination with the new 
config option, while the latter is prioritized.
Both places can, but don't need to define mount flags. If either of them
doesn't, it will simply not be taken into account.

Config file:

Adds a new config variable "GRUB_BTRFS_ROOTFLAGS":
- The new variable is used to include user-defined rootflags in the 
kernel command line of generated snapshot bootmenu entries.
See config file for more infos.
2021-01-11 13:37:40 +01:00

554 lines
21 KiB
Bash
Executable File

#! /usr/bin/env bash
#
#
# Written by: Antynea
# BTC donation address: 1Lbvz244WA8xbpHek9W2Y12cakM6rDe5Rt
# Github: https://github.com/Antynea/grub-btrfs
#
# Purpose:
# Improves Grub by adding "btrfs snapshots" to the Grub menu.
# You can start your system on a "snapshot" from the Grub menu.
# Supports manual snapshots, snapper ...
# Warning : booting on read-only snapshots can be tricky
# (Read about it, https://github.com/Antynea/grub-btrfs#warning-booting-on-read-only-snapshots-can-be-tricky)
#
# What this script does:
# - Automatically List snapshots existing on root partition (btrfs).
# - Automatically Detect if "/boot" is in separate partition.
# - Automatically Detect kernel, initramfs and intel microcode in "/boot" directory on snapshots.
# - Automatically Create corresponding "menuentry" in grub.cfg
# - Automatically detect snapper and use snapper's snapshot description if available.
# - Automatically generate grub.cfg if you use the provided systemd service.
#
# Installation:
# - Run `make install` or look into Makefile for instructions on where to put each file.
#
# Customization:
# Refer to config for the list of available options and their default values.
# Place your configurations to /etc/default/grub-btrfs/config.
#
# Automatically update Grub
# If you would like Grub to automatically update when a snapshots is made or deleted:
# - Use systemctl start/enable grub-btrfs.path
# - grub-btrfs.path automatically (re)generates grub.cfg when a modification appears in /.snapshots folder (by default).
# - If your snapshots aren't mounted in /.snapshots, you must modify the watch folder using systemctl edit grub-btrfs.path
# - See https://github.com/Antynea/grub-btrfs#automatically-update-grub
#
# Special thanks for assistance and contributions:
# - My friends
# - All contributors on Github
#
set -e
prefix="/usr"
exec_prefix="/usr"
datarootdir="/usr/share"
sysconfdir="/etc"
grub_btrfs_config="${sysconfdir}/default/grub-btrfs/config"
[[ -f "$grub_btrfs_config" ]] && . "$grub_btrfs_config"
. "$datarootdir/grub/grub-mkconfig_lib"
. "${sysconfdir}/default/grub"
### Variables in /etc/default/grub-btrfs/config
## Disable Grub-btrfs (default=active)
[[ "${GRUB_BTRFS_DISABLE:-"false"}" == "true" ]] && exit 0
## Submenu name
distro=$(awk -F "=" '/^NAME=/ {gsub(/"/, "", $2); print $2}' /etc/os-release)
submenuname=${GRUB_BTRFS_SUBMENUNAME:-"${distro:-Linux} snapshots"}
## Prefix entry
prefixentry=${GRUB_BTRFS_PREFIXENTRY:-"Snapshot:"}
## Limit snapshots to show in the Grub menu
limit_snap_show="${GRUB_BTRFS_LIMIT:-50}"
## How to sort snapshots list
btrfssubvolsort=(--sort="${GRUB_BTRFS_SUBVOLUME_SORT:-"-rootid"}")
## Snapper's config name
snapper_config=${GRUB_BTRFS_SNAPPER_CONFIG:-"root"}
## Customize GRUB directory, where "grub.cfg" file is saved
grub_directory=${GRUB_BTRFS_GRUB_DIRNAME:-"/boot/grub"}
## Customize BOOT directory, where kernels/initrams/microcode is saved.
boot_directory=${GRUB_BTRFS_BOOT_DIRNAME:-"/boot"}
## Password protection management for submenu
# Protection support for submenu (--unrestricted)
case "${GRUB_BTRFS_DISABLE_PROTECTION_SUBMENU:-"false"}" in
true) unrestricted_access_submenu="--unrestricted ";;
*) unrestricted_access_submenu=""
esac
# Authorized users (--users foo,bar)
if [ ! -z "${GRUB_BTRFS_PROTECTION_AUTHORIZED_USERS}" ] ; then
protection_authorized_users="--users ${GRUB_BTRFS_PROTECTION_AUTHORIZED_USERS} "
fi
### variables script
## Probe info "Boot partition"
# Boot device
boot_device=$(${grub_probe} --target=device ${boot_directory})
# hints string
boot_hs=$(${grub_probe} --device ${boot_device} --target="hints_string" 2>/dev/null)
# UUID of the boot partition
boot_uuid=$(${grub_probe} --device ${boot_device} --target="fs_uuid" 2>/dev/null)
# Type filesystem of boot partition
boot_fs=$(${grub_probe} --device ${boot_device} --target="fs" 2>/dev/null)
## Probe info "Root partition"
# Type filesystem of root partition
root_fs=$(${grub_probe} --target="fs" / 2>/dev/null)
# Root device
root_device=$(${grub_probe} --target=device /)
# UUID of the root partition
root_uuid=$(${grub_probe} --device ${root_device} --target="fs_uuid" 2>/dev/null)
## Parameters passed to the kernel
kernel_parameters="$GRUB_CMDLINE_LINUX $GRUB_CMDLINE_LINUX_DEFAULT"
## Mount point location
gbgmp=$(mktemp -d)
## Class for theme
CLASS="--class snapshots --class gnu-linux --class gnu --class os"
## save IFS
oldIFS=$IFS
## Detect uuid requirement (lvm,btrfs...)
check_uuid_required() {
if [ "x${root_uuid}" = "x" ] || [ "x${GRUB_DISABLE_LINUX_UUID}" = "xtrue" ] \
|| ! test -e "/dev/disk/by-uuid/${root_uuid}" \
|| ( test -e "${root_device}" && uses_abstraction "${root_device}" lvm ); then
LINUX_ROOT_DEVICE=${root_device}
else
LINUX_ROOT_DEVICE=UUID=${root_uuid}
fi
}
## Detect rootflags
detect_rootflags()
{
local fstabflags=$(grep -oE '^\s*[^#][[:graph:]]+\s+/\s+btrfs\s+[[:graph:]]+' "${gbgmp}/${snap_dir_name}/etc/fstab" \
| sed -E 's/^.*[[:space:]]([[:graph:]]+)$/\1/;s/,?subvol(id)?=[^,$]+//g;s/^,//')
rootflags="rootflags=${fstabflags:+$fstabflags,}${GRUB_BTRFS_ROOTFLAGS:+$GRUB_BTRFS_ROOTFLAGS,}"
}
### Error Handling
print_error()
{
local arg="$@"
local nothing_to_do="If you think an error has occurred , please file a bug report at \" https://github.com/Antynea/grub-btrfs \"\nNothing to do. Abort.\n"
printf "${arg}\n${nothing_to_do}" >&2 ;
exit 0
}
test_btrfs()
{
[[ "$root_fs" != "btrfs" ]] && print_error "Root partition isn't a btrfs filesystem.\nThis script only supports snapshots of the btrfs filesystem."
set +e
type btrfs >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
print_error "Unable to retrieve info from btrfs filesystem, make sure you have btrfs-progs on your system."
fi
set -e
}
### Script
## Create entry
entry()
{
echo "$@" >> "$grub_directory/grub-btrfs.cfg"
}
## menu entries
make_menu_entries()
{
## \" required for snap,kernels,init,microcode with space in their name
entry "submenu '$title_menu' {
submenu '---> $title_menu <---' { echo }"
for k in "${name_kernel[@]}"; do
[[ ! -f "${boot_dir}"/"${k}" ]] && continue;
kversion=${k#*"-"}
for i in "${name_initramfs[@]}"; do
prefix_i=${i%%"-"*}
suffix_i=${i#*"-"}
alt_suffix_i=${i##*"-"}
if [ "${kversion}" = "${suffix_i}" ]; then i="${i}";
elif [ "${kversion}.img" = "${suffix_i}" ]; then i="${i}";
elif [ "${kversion}-fallback.img" = "${suffix_i}" ]; then i="${i}";
elif [ "${kversion}.gz" = "${suffix_i}" ]; then i="${i}";
else continue ;
fi
for u in "${name_microcode[@]}"; do
if [[ "${name_microcode}" != "x" ]] ; then
entry "
menuentry '"${k}" & "${i}" & "${u}"' ${CLASS} "\$menuentry_id_option" 'gnulinux-snapshots-$boot_uuid' {"
else
entry "
menuentry '"${k}" & "${i}"' ${CLASS} "\$menuentry_id_option" 'gnulinux-snapshots-$boot_uuid' {"
fi
entry "\
if [ x\$feature_all_video_module = xy ]; then
insmod all_video
fi
set gfxpayload=keep
insmod ${boot_fs}
if [ x\$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root ${boot_hs} ${boot_uuid}
else
search --no-floppy --fs-uuid --set=root ${boot_uuid}
fi
echo 'Loading Snapshot: "${snap_date_time}" "${snap_dir_name}"'
echo 'Loading Kernel: "${k}" ...'
linux \"${boot_dir_root_grub}/"${k}"\" root="${LINUX_ROOT_DEVICE}" ${kernel_parameters} ${rootflags}subvol=\""${snap_dir_name}"\""
if [[ "${name_microcode}" != "x" ]] ; then
entry "\
echo 'Loading Microcode & Initramfs: "${u}" "${i}" ...'
initrd \"${boot_dir_root_grub}/"${u}"\" \"${boot_dir_root_grub}/"${i}"\""
else
entry "\
echo 'Loading Initramfs: "${i}" ...'
initrd \"${boot_dir_root_grub}/"${i}"\""
fi
entry " }"
count_warning_menuentries=$((1+$count_warning_menuentries))
done
done
done
entry "}"
}
## Trim a string from leading and trailing whitespaces
trim() {
local var="$*"
var="${var#"${var%%[![:space:]]*}"}"
var="${var%"${var##*[![:space:]]}"}"
echo -n "$var"
}
## List of snapshots on filesystem
snapshot_list()
{
# Query info from snapper if it is installed
type snapper >/dev/null 2>&1
if [ $? -eq 0 ]; then
if [ -s "/etc/snapper/configs/$snapper_config" ]; then
printf "Info: snapper detected, using config '$snapper_config'\n" >&2
local snapper_ids=($(snapper --no-dbus -t 0 -c "$snapper_config" list --disable-used-space | tail -n +3 | cut -d'|' -f 1))
local snapper_types=($(snapper --no-dbus -t 0 -c "$snapper_config" list --disable-used-space | tail -n +3 | cut -d'|' -f 2))
IFS=$'\n'
local snapper_descriptions=($(snapper --no-dbus -t 0 -c "$snapper_config" list --disable-used-space | tail -n +3 | rev | cut -d'|' -f 2 | rev))
else
printf "Warning: snapper detected but config '$snapper_config' does not exist\n" >&2
fi
fi
IFS=$'\n'
# Parse btrfs snapshots
local entries=()
local ids=()
local max_entry_length=0
for snap in $(btrfs subvolume list -sa "${btrfssubvolsort}" /); do
IFS=$oldIFS
snap=($snap)
local snap_path_name=${snap[@]:13:${#snap[@]}}
# Discard deleted snapshots
if [ "$snap_path_name" = "DELETED" ]; then continue; fi
[[ ${snap_path_name%%"/"*} == "<FS_TREE>" ]] && snap_path_name=${snap_path_name#*"/"}
# ignore specific path during run "grub-mkconfig"
if [ ! -z "${GRUB_BTRFS_IGNORE_SPECIFIC_PATH}" ] ; then
for isp in ${GRUB_BTRFS_IGNORE_SPECIFIC_PATH[@]} ; do
[[ "${snap_path_name}" == "${isp}" ]] && continue 2;
done
fi
if [ ! -z "${GRUB_BTRFS_IGNORE_PREFIX_PATH}" ] ; then
for isp in ${GRUB_BTRFS_IGNORE_PREFIX_PATH[@]} ; do
[[ "${snap_path_name}" == "${isp}"/* ]] && continue 2;
done
fi
# detect if /boot directory exists
[[ ! -d "$gbgmp/$snap_path_name/boot" ]] && continue;
local id="${snap_path_name//[!0-9]}" # brutal way to get id: remove everything non-numeric
ids+=("$id")
local entry="${snap[@]:10:2} | ${snap_path_name}"
entries+=("$entry")
# Find max length of a snapshot entry, needed for pretty formatting
local length="${#entry}"
[[ "$length" -gt "$max_entry_length" ]] && max_entry_length=$length
done
# Find max length of a snapshot type, needed for pretty formatting
local max_type_length=0
for id in "${ids[@]}"; do
for j in "${!snapper_ids[@]}"; do
local snapper_id="${snapper_ids[$j]//[[:space:]]/}"
if [[ "$snapper_id" == "$id" ]]; then
local snapper_type=$(trim "${snapper_types[$j]}")
local length="${#snapper_type}"
[[ "$length" -gt "$max_type_length" ]] && max_type_length=$length
fi
done
done
for i in "${!entries[@]}"; do
local id="${ids[$i]}"
local entry="${entries[$i]}"
for j in "${!snapper_ids[@]}"; do
local snapper_id="${snapper_ids[$j]//[[:space:]]/}"
# remove other non numeric characters
snapper_id="${snapper_id//\*/}"
snapper_id="${snapper_id//\+/}"
snapper_id="${snapper_id//-/}"
if [[ "$snapper_id" == "$id" ]]; then
local snapper_type=$(trim "${snapper_types[$j]}")
local snapper_description=$(trim "${snapper_descriptions[$j]}")
printf -v entry "%-${max_entry_length}s | %-${max_type_length}s | %s" "$entry" "$snapper_type" "$snapper_description"
break
fi
done
echo "$entry"
done
IFS=$oldIFS
}
## Detect kernels in "/boot"
detect_kernel()
{
list_kernel=()
# Original kernel (auto-detect)
for okernel in "${boot_dir}"/vmlinuz-* \
"${boot_dir}"/vmlinux-* \
"${boot_dir}"/kernel-* ; do
[[ ! -f "${okernel}" ]] && continue;
list_kernel+=("$okernel")
done
# Custom name kernel in GRUB_BTRFS_NKERNEL
if [ ! -z "${GRUB_BTRFS_NKERNEL}" ] ; then
for ckernel in "${boot_dir}/${GRUB_BTRFS_NKERNEL[@]}" ; do
[[ ! -f "${ckernel}" ]] && continue;
list_kernel+=("$ckernel")
done
fi
}
## Detect initramfs in "/boot"
detect_initramfs()
{
list_initramfs=()
# Original initramfs (auto-detect)
for oinitramfs in "${boot_dir}"/initrd.img-* \
"${boot_dir}"/initrd-*.img \
"${boot_dir}"/initrd-*.gz \
"${boot_dir}"/initramfs-*.img \
"${boot_dir}"/initramfs-*.gz ; do
[[ ! -f "${oinitramfs}" ]] && continue;
list_initramfs+=("$oinitramfs")
done
# Custom name initramfs in GRUB_BTRFS_NINIT
if [ ! -z "${GRUB_BTRFS_NINIT}" ] ; then
for cinitramfs in "${boot_dir}/${GRUB_BTRFS_NINIT[@]}" ; do
[[ ! -f "${cinitramfs}" ]] && continue;
list_initramfs+=("$cinitramfs")
done
fi
}
## Detect microcode in "/boot"
detect_microcode()
{
list_ucode=()
# Original intel/amd microcode (auto-detect)
# See "https://www.gnu.org/software/grub/manual/grub/html_node/Simple-configuration.html"
for oiucode in "${boot_dir}"/intel-uc.img \
"${boot_dir}"/intel-ucode.img \
"${boot_dir}"/amd-uc.img \
"${boot_dir}"/amd-ucode.img \
"${boot_dir}"/early_ucode.cpio \
"${boot_dir}"/microcode.cpio; do
[[ ! -f "${oiucode}" ]] && continue;
list_ucode+=("$oiucode")
done
# Custom name microcode in GRUB_BTRFS_CUSTOM_MICROCODE
if [ ! -z "${GRUB_BTRFS_CUSTOM_MICROCODE}" ] ; then
for cucode in "${boot_dir}/${GRUB_BTRFS_CUSTOM_MICROCODE[@]}" ; do
[[ ! -f "${cucode}" ]] && continue
list_ucode+=("$cucode")
done
fi
if [ -z "${list_ucode}" ]; then list_ucode=(x); fi
}
## Show full path snapshot or only name
path_snapshot()
{
case "${GRUB_BTRFS_DISPLAY_PATH_SNAPSHOT:-"true"}" in
true) name_snapshot=("${snap_full_name}");;
*) name_snapshot=("${snap_full_name#*"/"}")
esac
}
## Title format in grub-menu
title_format()
{
case "${GRUB_BTRFS_TITLE_FORMAT:-"p/d/n"}" in
p/n/d) title_menu="${prefixentry} ${name_snapshot} ${snap_date_time}";;
p/d) title_menu="${prefixentry} ${snap_date_time}";;
p/n) title_menu="${prefixentry} ${name_snapshot}";;
d/n) title_menu="${snap_date_time} ${name_snapshot}";;
n/d) title_menu="${name_snapshot} ${snap_date_time}";;
p) title_menu="${prefixentry}";;
d) title_menu="${snap_date_time}";;
n) title_menu="${name_snapshot}";;
*) title_menu="${prefixentry} ${snap_date_time} ${name_snapshot}"
esac
}
## List of kernels, initramfs and microcode in snapshots
boot_bounded()
{
# Initialize menu entries
IFS=$'\n'
for item in $(snapshot_list); do
[[ ${limit_snap_show} -le 0 ]] && break; # fix: limit_snap_show=0
IFS=$oldIFS
snap_full_name="$(echo "$item" | cut -d'|' -f2-)" # do not trim it to keep nice formatting
snap_dir_name="$(echo "$item" | cut -d'|' -f2)"
snap_dir_name="$(trim "$snap_dir_name")"
snap_date_time="$(echo "$item" | cut -d' ' -f1-2)"
snap_date_time="$(trim "$snap_date_time")"
boot_dir="$gbgmp/$snap_dir_name$boot_directory"
# Kernel (Original + custom kernel)
detect_kernel
if [ -z "${list_kernel}" ]; then continue; fi
name_kernel=("${list_kernel[@]##*"/"}")
# Detect rootflags
detect_rootflags
# Initramfs (Original + custom initramfs)
detect_initramfs
if [ -z "${list_initramfs}" ]; then continue; fi
name_initramfs=("${list_initramfs[@]##*"/"}")
# microcode (auto-detect + custom microcode)
detect_microcode
name_microcode=("${list_ucode[@]##*"/"}")
# show snapshot found during run "grub-mkconfig"
if [[ "${GRUB_BTRFS_SHOW_SNAPSHOTS_FOUND:-"true"}" = "true" ]]; then
printf "Found snapshot: %s\n" "$item" >&2 ;
fi
# Show full path snapshot or only name
path_snapshot
# Title format in grub-menu
title_format
# convert /boot directory to root of GRUB (e.g /boot become /)
boot_dir_root_grub="$(make_system_path_relative_to_its_root "${boot_dir}")"
# Make menuentries
make_menu_entries
### Limit snapshots found during run "grub-mkconfig"
count_limit_snap=$((1+$count_limit_snap))
[[ $count_limit_snap -ge $limit_snap_show ]] && break;
# Limit generation of menuentries if exceeds 250
# [[ $count_warning_menuentries -ge 250 ]] && break;
done
IFS=$oldIFS
}
boot_separate()
{
boot_dir="${boot_directory}"
# convert /boot directory to root of GRUB (e.g /boot become /)
boot_dir_root_grub="$(make_system_path_relative_to_its_root "${boot_dir}")"
# Kernel (Original + custom kernel)
detect_kernel
if [ -z "${list_kernel}" ]; then print_error "Kernels not found."; fi
name_kernel=("${list_kernel[@]##*"/"}")
# Initramfs (Original + custom initramfs)
detect_initramfs
if [ -z "${list_initramfs}" ]; then print_error "Initramfs not found."; fi
name_initramfs=("${list_initramfs[@]##*"/"}")
# microcode (auto-detect + custom microcode)
detect_microcode
name_microcode=("${list_ucode[@]##*"/"}")
# Initialize menu entries
IFS=$'\n'
for item in $(snapshot_list); do
[[ ${limit_snap_show} -le 0 ]] && break; # fix: limit_snap_show=0
IFS=$oldIFS
snap_full_name="$(echo "$item" | cut -d'|' -f2-)" # do not trim it to keep nice formatting
snap_dir_name="$(echo "$item" | cut -d'|' -f2)"
snap_dir_name="$(trim "$snap_dir_name")"
snap_date_time="$(echo "$item" | cut -d' ' -f1-2)"
snap_date_time="$(trim "$snap_date_time")"
# Detect rootflags
detect_rootflags
# show snapshot found during run "grub-mkconfig"
if [[ "${GRUB_BTRFS_SHOW_SNAPSHOTS_FOUND:-"true"}" = "true" ]]; then
printf "Found snapshot: %s\n" "$item" >&2 ;
fi
# Show full path snapshot or only name
path_snapshot
# Title format in grub-menu
title_format
# Make menuentries
make_menu_entries
# Limit snapshots found during run "grub-mkconfig"
count_limit_snap=$((1+$count_limit_snap))
[[ $count_limit_snap -ge $limit_snap_show ]] && break;
# Limit generation of menuentries if exceeds 250
# [[ $count_warning_menuentries -ge 250 ]] && break;
done
IFS=$oldIFS
}
### Start
printf "Detecting snapshots ...\n" >&2 ;
# Only support btrfs snapshots
test_btrfs
# Delete existing config
#rm -f --preserve-root "$grub_directory/grub-btrfs.cfg"
> "$grub_directory/grub-btrfs.cfg"
# Create mount point then mounting
[[ ! -d $gbgmp ]] && mkdir -p $gbgmp
mount -o subvolid=5 /dev/disk/by-uuid/$root_uuid $gbgmp/
# Count menuentries
count_warning_menuentries=0
# Count snapshots
count_limit_snap=0
# detect uuid requirement
check_uuid_required
# Detects if /boot is a separate partition
if [[ "${GRUB_BTRFS_OVERRIDE_BOOT_PARTITION_DETECTION:-"false"}" == "true" ]]; then
printf "Info: Override boot partition detection : enable \n" >&2 ;
boot_separate
else
if [[ "$root_uuid" != "$boot_uuid" ]]; then
printf "Info: Separate boot partition detected \n" >&2 ;
boot_separate
else
printf "Info: Separate boot partition not detected \n" >&2 ;
boot_bounded
fi
fi
# unmounting mount point
umount $gbgmp
# Show warn, menuentries exceeds 250 entries
[[ $count_warning_menuentries -ge 250 ]] && printf "Generated ${count_warning_menuentries} total GRUB entries. You might experience issues loading snapshots menu in GRUB.\n" >&2 ;
# printf "menuentries = $count_warning_menuentries \n" >&2 ;
# Show total found snapshots
if [[ "${GRUB_BTRFS_SHOW_TOTAL_SNAPSHOTS_FOUND:-"true"}" = "true" && ! -z "${count_limit_snap}" && "${count_limit_snap}" != "0" ]]; then
printf "Found ${count_limit_snap} snapshot(s)\n" >&2 ;
fi
# if no snapshot found, exit
if [[ "${count_limit_snap}" = "0" || -z "${count_limit_snap}" ]]; then
print_error "No snapshots found."
fi
# Make a submenu in GRUB (grub.cfg)
cat << EOF
submenu '${submenuname}' ${protection_authorized_users}${unrestricted_access_submenu}{
configfile "\${prefix}/grub-btrfs.cfg"
}
EOF
### End