Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions fedora/bootmenu/dracut/90ryoku-snapshot/module-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/bash
#
# dracut module: boots a snapper snapshot from the "Ryoku snapshots" GRUB
# menu, read-only or restored. It runs after dracut has unlocked LUKS, so an
# encrypted disk restores the same way as a plain one. Only the images
# `ryoku boot-menu sync` builds for the snapshot entries carry it (dracut
# --add ryoku-snapshot); Fedora's own initramfs images stay as they are.

check() {
return 255
}

depends() {
echo btrfs
}

installkernel() {
hostonly='' instmods overlay
}

# shellcheck disable=SC2154 # dracut sets moddir
install() {
inst_multiple mkdir mount umount mv date ln
inst_hook pre-mount 90 "$moddir/ryoku-snapshot-restore.sh"
inst_hook pre-pivot 10 "$moddir/ryoku-snapshot-preview.sh"
}
44 changes: 44 additions & 0 deletions fedora/bootmenu/dracut/90ryoku-snapshot/ryoku-snapshot-preview.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/bash
#
# pre-pivot hook. rd.ryoku.preview=<top-level path of a snapshot> boots that
# read-only snapshot for a look: the snapshot is the lower layer of an overlay
# whose writes go to RAM, so the system runs normally and nothing on disk
# changes. sysroot.mount has already mounted the snapshot read-only (the
# entry's rootflags name it); the overlay is stacked on top of /sysroot, and
# its lower layer is a second mount under /run, which switch-root carries
# into the booted system. fstab's options for / name a btrfs subvolume, which
# the overlay refuses on remount, so systemd-remount-fs is masked for this
# boot only (/run).
#
# dracut sources hooks, so this returns and never exits.

# shellcheck source=/dev/null
type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh

ryoku_snapshot_preview() {
local snap dev run="${RYOKU_RUN:-/run}" newroot="${NEWROOT:-/sysroot}"
local base="$run/ryoku/preview"
snap=$(getarg rd.ryoku.preview=) || return 0
case "$snap" in
"" | /* | *..* | *[!A-Za-z0-9._/-]*) warn "ryoku: refusing snapshot path '$snap'"; return 0 ;;
esac
dev=$(label_uuid_to_dev "$(getarg root=)")
mkdir -p "$base/lower" "$base/rw"
if ! mount -t btrfs -o "ro,subvol=$snap" "$dev" "$base/lower"; then
warn "ryoku: cannot mount $snap; booting it read-only as it is"
return 0
fi
mount -t tmpfs -o mode=0755 ryoku-preview "$base/rw"
mkdir -p "$base/rw/upper" "$base/rw/work"
if ! mount -t overlay ryoku-preview \
-o "lowerdir=$base/lower,upperdir=$base/rw/upper,workdir=$base/rw/work" "$newroot"; then
warn "ryoku: overlay failed; booting $snap read-only as it is"
umount "$base/rw" "$base/lower"
return 0
fi
mkdir -p "$run/systemd/system"
ln -sf /dev/null "$run/systemd/system/systemd-remount-fs.service"
info "ryoku: previewing $snap; changes stay in RAM"
}

ryoku_snapshot_preview
60 changes: 60 additions & 0 deletions fedora/bootmenu/dracut/90ryoku-snapshot/ryoku-snapshot-restore.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/bash
#
# pre-mount hook. rd.ryoku.restore=<top-level path of a snapshot> makes that
# snapshot the root subvolume again before it is mounted: the current root is
# renamed to <root>.broken-<time> (kept, so a mistaken restore is itself
# reversible) and a writable snapshot of the chosen one takes its name. The
# root subvolume is the one rootflags=subvol= names. Anything unexpected
# leaves the disk untouched and boots the current root.
#
# dracut sources hooks, so this returns and never exits.

# shellcheck source=/dev/null
type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh

ryoku_snapshot_restore() {
local snap rootsub dev top stamp run="${RYOKU_RUN:-/run}"
snap=$(getarg rd.ryoku.restore=) || return 0
case "$snap" in
"" | /* | *..* | *[!A-Za-z0-9._/-]*) warn "ryoku: refusing snapshot path '$snap'"; return 0 ;;
*/snapshot) ;;
*) warn "ryoku: '$snap' is not a snapper snapshot"; return 0 ;;
esac
rootsub=$(getarg rootflags=)
case "$rootsub" in *subvol=*) ;; *) rootsub="" ;; esac
rootsub=${rootsub#*subvol=}
rootsub=${rootsub%%,*}
rootsub=${rootsub#/}
case "$rootsub" in
"" | *..* | */*) warn "ryoku: no root subvolume in rootflags; not restoring"; return 0 ;;
esac
dev=$(label_uuid_to_dev "$(getarg root=)")
[ -e "$dev" ] || { warn "ryoku: root device '$dev' not found; not restoring"; return 0; }

top="$run/ryoku/restore-top"
mkdir -p "$top"
mount -t btrfs -o subvolid=5 "$dev" "$top" || { warn "ryoku: cannot mount the btrfs top level"; return 0; }
if [ ! -d "$top/$snap" ] || [ ! -d "$top/$rootsub" ]; then
warn "ryoku: $snap or $rootsub is missing; not restoring"
umount "$top"
return 0
fi
stamp=$(date -u +%Y%m%d-%H%M%S)
if ! mv "$top/$rootsub" "$top/$rootsub.broken-$stamp"; then
warn "ryoku: could not set the current root aside; not restoring"
umount "$top"
return 0
fi
if ! btrfs subvolume snapshot "$top/$snap" "$top/$rootsub" >/dev/null; then
warn "ryoku: snapshotting $snap failed; putting the current root back"
mv "$top/$rootsub.broken-$stamp" "$top/$rootsub"
umount "$top"
return 0
fi
umount "$top"
info "ryoku: restored $snap; the previous root is $rootsub.broken-$stamp"
mkdir -p "$run/ryoku"
echo "$snap $rootsub.broken-$stamp" >"$run/ryoku/restored"
}

ryoku_snapshot_restore
13 changes: 13 additions & 0 deletions fedora/bootmenu/grub/42_ryoku_snapshots
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/sh
#
# grub2-mkconfig snippet (/etc/grub.d/42_ryoku_snapshots). It only sources
# ryoku-snapshots.cfg next to grub.cfg, the "Ryoku snapshots" submenu that
# `ryoku boot-menu sync` rewrites after every snapshot, so a new snapshot never
# needs a grub2-mkconfig run and nothing touches Fedora's BLS entries.
set -e

cat <<'EOF'
if [ -f "${config_directory}/ryoku-snapshots.cfg" ]; then
source "${config_directory}/ryoku-snapshots.cfg"
fi
EOF
96 changes: 96 additions & 0 deletions fedora/bootmenu/grub/ryoku-grub-menu
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/bash
#
# Puts Ryoku into Fedora's GRUB menu, keeping shim, GRUB and the kernels
# exactly as Fedora signs them (issue #48).
#
# install copy the theme onto /boot, point /etc/default/grub at it, and
# rebuild grub.cfg when either changed or grub.cfg does not
# source the snapshot submenu yet (ryoku-desktop %posttrans)
# purge undo all of it and drop the snapshot menu and its kernels
# (ryoku-desktop %preun on removal; %postun rebuilds grub.cfg)
#
# The theme's font is a copy of GRUB's Unifont on /boot: left to itself,
# grub2-mkconfig picks one from /usr/share/grub, which sits on the root
# filesystem GRUB cannot read on an encrypted install.
set -euo pipefail

grub_dir="${RYOKU_GRUB_DIR:-/boot/grub2}"
defaults="${RYOKU_GRUB_DEFAULTS:-/etc/default/grub}"
shipped="${RYOKU_GRUB_THEME_SRC:-/usr/share/ryoku/grub/theme}"
unifont="${RYOKU_GRUB_FONT_SRC:-/usr/share/grub/unicode.pf2}"
theme_dir="$grub_dir/themes/ryoku"
mkconfig="${RYOKU_GRUB_MKCONFIG:-grub2-mkconfig}"

# set_key <KEY> <value>: set KEY="value" in /etc/default/grub; returns 0 when
# the file changed.
set_key() {
local line="$1=\"$2\""
grep -qxF -- "$line" "$defaults" && return 1
if grep -q "^$1=" "$defaults"; then
sed -i "s|^$1=.*|$line|" "$defaults"
else
printf '%s\n' "$line" >>"$defaults"
fi
}

# terminal_output: the current GRUB_TERMINAL_OUTPUT value, unquoted.
terminal_output() {
sed -n 's/^GRUB_TERMINAL_OUTPUT="\{0,1\}\([^"]*\)"\{0,1\}$/\1/p' "$defaults"
}

drop_key() {
grep -q "^$1=" "$defaults" || return 1
sed -i "/^$1=/d" "$defaults"
}

# copy_new <src> <dest>: copy when the content differs; returns 0 when it did.
copy_new() {
cmp -s "$1" "$2" && return 1
cp -f "$1" "$2"
}

rebuild() {
"$mkconfig" -o "$grub_dir/grub.cfg" >/dev/null
}

install_menu() {
[ -d "$grub_dir" ] && [ -f "$defaults" ] || return 0
mkdir -p "$theme_dir"
# grub2-mkconfig loads the image modules the theme directory needs, so a
# changed theme rebuilds grub.cfg too.
local changed=1 file
for file in "$shipped"/*; do
copy_new "$file" "$theme_dir/${file##*/}" && changed=0
done
[ -f "$unifont" ] && cp -f "$unifont" "$theme_dir/unicode.pf2"

# only Fedora's plain console becomes graphical; a serial or other chosen
# terminal is kept, and the theme simply does not show there.
case "$(terminal_output)" in
"" | console) set_key GRUB_TERMINAL_OUTPUT gfxterm && changed=0 ;;
esac
set_key GRUB_FONT "$theme_dir/unicode.pf2" && changed=0
set_key GRUB_THEME "$theme_dir/theme.txt" && changed=0
if [ "$changed" = 0 ] || ! grep -qs "ryoku-snapshots.cfg" "$grub_dir/grub.cfg"; then
rebuild
fi
}

purge_menu() {
[ -f "$defaults" ] || return 0
if grep -qxF "GRUB_THEME=\"$theme_dir/theme.txt\"" "$defaults"; then
drop_key GRUB_THEME || :
drop_key GRUB_FONT || :
[ "$(terminal_output)" = gfxterm ] && set_key GRUB_TERMINAL_OUTPUT console || :
fi
rm -rf "$theme_dir" "$grub_dir/ryoku-snapshots.cfg" "$(dirname "$grub_dir")/ryoku"
}

case "${1:-}" in
install) install_menu ;;
purge) purge_menu ;;
*)
echo "usage: ryoku-grub-menu install|purge" >&2
exit 2
;;
esac
Binary file added fedora/bootmenu/grub/theme/background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions fedora/bootmenu/grub/theme/theme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Ryoku GRUB theme: the desktop's default palette (paper #000000, ink
# #cdc4ba, sun #e2342a) on the Unifont GRUB ships. Installed to
# /boot/grub2/themes/ryoku by ryoku-grub-menu. The background is a black
# image as well as a colour because GRUB, left with only a colour, fails to
# scale a missing bitmap whenever an entry boots from a submenu.

title-text: "Ryoku"
title-font: "Unifont Regular 16"
title-color: "#e2342a"
desktop-color: "#000000"
desktop-image: "background.png"
message-font: "Unifont Regular 16"
message-color: "#b0a9a0"
terminal-font: "Unifont Regular 16"

+ boot_menu {
left = 15%
top = 20%
width = 70%
height = 60%
item_font = "Unifont Regular 16"
item_color = "#cdc4ba"
selected_item_font = "Unifont Regular 16"
selected_item_color = "#e2342a"
item_height = 24
item_padding = 4
item_spacing = 4
scrollbar = false
}

+ label {
id = "__timeout__"
left = 15%
top = 85%
width = 70%
align = "center"
font = "Unifont Regular 16"
color = "#b0a9a0"
text = "Booting in %d s"
}

+ label {
left = 15%
top = 90%
width = 70%
align = "center"
font = "Unifont Regular 16"
color = "#b0a9a0"
text = "Enter boots e edits c opens a console"
}
12 changes: 12 additions & 0 deletions fedora/bootmenu/ryoku-snapshot-menu.path
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Unit]
# Snapper changes /.snapshots while it takes a snapshot, and its Ryoku plugin
# touches it once the snapshot is complete.
Description=Ryoku snapshot boot menu trigger
RequiresMountsFor=/.snapshots

[Path]
PathChanged=/.snapshots
Unit=ryoku-snapshot-menu.service

[Install]
WantedBy=paths.target
17 changes: 17 additions & 0 deletions fedora/bootmenu/ryoku-snapshot-menu.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[Unit]
# Started by ryoku-snapshot-menu.path after a snapshot is created or deleted,
# and by ryoku-desktop's %posttrans. Rewrites the "Ryoku snapshots" GRUB submenu and
# keeps one kernel and initramfs copy per kernel version the snapshots need.
Description=Ryoku snapshot boot menu
# A dnf transaction or a snapper cleanup changes /.snapshots many times within
# a second. Each run is an idempotent resync, so the default start limit would
# only fail the unit and take the path trigger down with it.
StartLimitIntervalSec=0
ConditionPathExists=/etc/snapper/configs/root
RequiresMountsFor=/boot /.snapshots

[Service]
Type=oneshot
ExecStart=/usr/bin/ryoku boot-menu sync
Nice=10
IOSchedulingClass=idle
16 changes: 16 additions & 0 deletions fedora/bootmenu/ryoku-snapshot-restored.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[Unit]
# Runs on the first boot after the initramfs restored a snapshot (the dracut
# module leaves /run/ryoku/restored). /boot is not part of the snapshot, so it
# still lists kernels the restored root has no modules for: those entries are
# dropped, the restored root's own kernels get entries, and the kernel this
# boot runs becomes the default.
Description=Ryoku boot entries after a snapshot restore
ConditionPathExists=/run/ryoku/restored
RequiresMountsFor=/boot

[Service]
Type=oneshot
ExecStart=/usr/bin/ryoku boot-menu restored

[Install]
WantedBy=multi-user.target
17 changes: 17 additions & 0 deletions fedora/bootmenu/snapper/50-ryoku-boot-menu
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/sh
#
# snapper plugin (/usr/libexec/snapper/plugins/). snapperd runs it around
# every snapshot and config change with the event as its first argument. After
# a snapshot is created or deleted it touches /.snapshots, and
# ryoku-snapshot-menu.path resyncs the boot menu in the background, so building
# a kernel image for a new kernel never holds up the snapshot (or the
# `ryoku update` that took it). SELinux lets snapperd neither start units nor
# write a file systemd may watch, but systemd may watch /.snapshots itself.

snapshots="${RYOKU_SNAPSHOTS_DIR:-/.snapshots}"
case "${1:-}" in
create-snapshot-post | delete-snapshot-post)
touch "$snapshots" 2>/dev/null || :
;;
esac
exit 0
Loading
Loading