Repository navigation
Releases: jccl1706/btrfs-patrol
Release list
btrfs-patrol 0.8.0
Colour in the terminal interface, and the two things a screenshot of it found that 433 tests could not.
New
The interface is in colour. rollback snapshots are warned about, the dnf- kinds accented and timer dimmed; a kept snapshot's * is green; a result is green and a failure red; the title and the key line are bands across the width, and the cursor's row is a shade off the background where the terminal has 256 colours and reverse video where it does not.
Nothing is distinguished by colour alone — the cursor keeps its ▸, the bands keep reverse video, the header keeps A_DIM — which is what makes --color never a usable setting rather than a broken screen. NO_COLOR, TERM=dumb and output.color are honoured, the same as every other command.
Underneath, Screen.render_styled() is now the renderer and render() joins its spans, so the plain and the coloured screen cannot disagree; tui.Palette is the only thing that knows what a colour is, which keeps the decisions testable without curses.
The rules between the regions are gone, and the table has the rows back. The bands are a stronger edge than a line of ─, and three rules were spending three of a terminal's rows to repeat what the colour already says.
A wider gap before DESCRIPTION. rollback fills the KIND column exactly, so at two spaces the longest word in the table sat hard against a sentence. At exactly 80 columns this costs the KERNEL column — which is the trade the table already prefers, since a kernel version rarely identifies a snapshot while a description usually does. Above 80 columns nothing is lost.
Fixed
The key bar was missing on a full screen. CHROME_HEIGHT was 6 against a render of 8, so the table was handed two rows that did not exist, the screen came out two lines taller than the window, and the bottom was truncated. Anyone with more snapshots than rows had never seen the key bar — it looked like a deliberately bare screen. Every test used a handful of snapshots, where nothing overflows.
Every kept snapshot drew a blank row. The ID cell of a kept snapshot splits into the padding before the *, the *, and the digits after it; where the number leaves no padding the first of those is empty, and the painter treated the first empty span as the end of the line. The view model had the text right, so no test could see it — it was found in a screenshot of a store whose only snapshot was kept.
The bands floated up the screen. The table emitted only as many rows as it had snapshots, so a short list put the key bar across the seventh line of a full-height terminal with the desktop showing beneath it. The table now keeps its rows whether or not it has anything to put in them.
--color never was ignored by the interface alone. The flag overrides the Console and leaves output.color untouched; the palette read the configuration. It reads the console now.
Also
The README shows the interface, as an SVG drawn from the view model by tools/tui-preview.py rather than photographed — re-run it and the picture is current. It cannot show a painter bug, which is exactly how the blank rows survived, and docs/tui-design.md says so.
433 tests, up from 412.
btrfs-patrol 0.7.1
A fix for snapshot boot entries that GRUB could not find, and what walking the whole rescue path on real hardware taught.
Fixed
Boot entries hung GRUB where /boot is not its own partition. The screen stopped at Welcome to GRUB! with no message and no working keyboard. For a feature whose purpose is rescuing a machine that will not boot, that is the worst failure it could have, and 0.7.0 shipped it.
The Boot Loader Specification says a Type #1 entry's linux path is relative to $BOOT, and where /boot is its own partition that is what everything agrees on — linux /vmlinuz-6.19.10. Where /boot is a directory or subvolume of the root filesystem, as Fedora's Cloud image lays it out, GRUB opens the whole filesystem and resolves from its root, so kernel-install writes linux /boot/vmlinuz-7.2.5 instead.
The path is now copied from an entry kernel-install already wrote rather than derived. Those entries are the authority on what the bootloader expects, and there is always at least one or the machine could not boot.
If you have [boot] entries enabled and /boot is not a separate partition, upgrade before you need it. Where /boot is its own partition, the entries were always correct and nothing changes.
Documented
The rescue path was walked end to end on real hardware — a ThinkPad with a LUKS root and its own /boot: rm -rf /etc, reboot, pick the snapshot from the GRUB menu, btrfs-patrol rollback, reboot, working system. Three things came out of it that the manual did not say:
findmnt /reports a booted snapshot asrw, and writes still fail. The kernel mounts itro;systemd-remount-fsthen applies/etc/fstab, which says nothing aboutro, and remounts it read-write. The writes fail anyway, because the subvolume carries btrfs's read-only property. Not a bug to go chasing.auditd,audit-rulesandtunedfail in such a session, and systemd reports the machine asdegraded. That is what a read-only root means, not a second fault.- Pick the entry from the GRUB menu rather than using
grub2-reboot. Its one-shot is meant to be consumed by the next boot and was not: the machine went on booting the snapshot every restart untilnext_entrywas cleared by hand. Choosing the entry at the menu never sets it.
Also recorded: on Fedora's GRUB the menu is hidden after a boot that worked (menu_auto_hide), and snapshot entries sort above the normal one because blscfg ignores the sort-key that orders them in systemd-boot. Neither changes what boots by default — GRUB_DEFAULT=saved records the entry by id, not by position.
btrfs-patrol 0.7.0
A terminal interface, and a way to see what changed between two snapshots. This finishes the roadmap.
New
btrfs-patrol tui
btrfs-patrol subvolume: all subvolumes
───────────────────────────────────────────────────────────────────────────────────
ID DATE TIME KERNEL KIND DESCRIPTION
▸ 1 2026-09-21 21:59:31 6.19.10-300.fc44.x86_64 manual known good
*3 2026-09-21 22:01:04 6.19.10-300.fc44.x86_64 rollback state before rollback
───────────────────────────────────────────────────────────────────────────────────
#1 · root · 2026-09-21 21:59:31 · 6.19.10-300.fc44.x86_64 · manual
known good
───────────────────────────────────────────────────────────────────────────────────
↑↓ move ⇥ subvol n new d describe k keep c compare x delete ? help q quit
Browse the snapshots of every configured subvolume, open one in full, and take, describe, keep and delete them. / filters with the same selectors list takes. ? lists the keys.
Rrolls back — shift, deliberately, because it is the one key that replaces a subvolume. It shows the plan first: which subvolume, how root is found at boot, whether the running kernel has its modules in the snapshot and a boot entry, and which nested subvolumes move with it. Every check is the onebtrfs-patrol rollbackalready makes.ccompares — mark a snapshot, presscon another.- It uses Python's own curses, so btrfs-patrol still needs nothing beyond Python,
btrfs-progsandutil-linux.
btrfs-patrol diff OLD NEW
What changed between two snapshots of the same subvolume:
$ sudo btrfs-patrol diff 1 2
:: snapshot 1 -> 2 (root): 3 added, 2 removed, 1 modified, 1 renamed, 1 attrs
+ difftest/added-dir
+ difftest/added.txt
- difftest/deleted.txt
~ difftest/modified.txt
> difftest/oldname.txt -> difftest/newname.txt
a difftest/unchanged.txtIt asks btrfs, with btrfs send --no-data piped into btrfs receive --dump, so no file contents are read and the cost is in metadata rather than the size of the tree. Either order works, and reversing the arguments reverses the answer. Timestamps alone are not counted as a change: running a program updates its access time, so a comparison across any real work would otherwise be mostly binaries that were merely executed.
Also
listno longer runs past the width it was given. Columns are dropped when they do not fit — KERNEL first, then TIME — because a long kernel version could leave the description at its 20-column floor and push the row off the edge. The interface andlistnow share one table layout, so they cannot disagree about what a snapshot looks like.- Status messages flush stdout first, so a piped or redirected run no longer shows a warning before the plan it refers to. This affected
rollbackas well.
Tested
407 unit tests, plus the interface driven in a real terminal and checked against a real snapshot store — on a Fedora VM and on a ThinkPad with a LUKS root, its own /boot partition and a nested var/lib/machines subvolume. A rollback was taken through the interface and the machine rebooted into the restored state.
btrfs-patrol 0.6.0
Turn an existing directory into a subvolume, so it can be snapshotted and rolled back on its own.
New
$ sudo btrfs-patrol convert /var/log
directory: /var/log (412.8 MiB, 1,203 files)
filesystem: /dev/vda2, as root/var/log from the top level
new subvolume: nested in place, no /etc/fstab entry needed
plan:
1. create a subvolume at /var/.log.new-subvolume
2. copy the contents into it, sharing extents (reflink)
3. move /var/log aside to /var/.log.pre-subvolume
4. move the subvolume into place as /var/log
5. add [subvolumes.var-log] to /etc/btrfs-patrol/config.toml
Convert /var/log to subvolume 'var-log'? [y/N]A directory inside the root subvolume goes wherever root goes. convert makes it a subvolume of its own, which btrfs-patrol then manages like any other.
- Nested, not mounted. The subvolume is created where the directory was, so there is no
/etc/fstabentry and no mount unit — nothing that a failed boot would make hard to undo. Snapshots of root stop including it, and rolling root back moves it across untouched, so the logs you want to read after a rollback survive it. - Reflinked copy, so converting a large directory costs almost no space or time. Permissions, symlinks, hidden files and SELinux contexts are preserved.
- Processes holding the directory open are named afterwards, with a
systemctl restartline for the services that will accept one. Nothing is restarted for you, and the previous contents are kept rather than deleted, so anything written in the meantime can still be recovered. - A unit that refuses a manual restart is named separately.
auditdsetsRefuseManualStartand holds/var/log, so including it in the restart line made the whole command fail and reopen nothing. Only a reboot moves it across, and the tool now says so. - It refuses
/,/boot,/etc,/usrand the pseudo-filesystems, a path that is already a subvolume, a mount point, a symlink, and anything not on the btrfs filesystem mounted at/.
--name sets the subvolume's name, --no-config prints the [subvolumes.NAME] table instead of writing it, and --dry-run runs every check and changes nothing.
Also
- Status messages flush stdout first, so a piped or redirected run no longer shows a warning before the plan it refers to. This affected
rollbacktoo. tests/check-convert.shchecksconvertend to end against a real btrfs root — the part unit tests cannot reach. Verified on Fedora 44, including converting a live/var/logwith journald and auditd holding it, and a reboot afterwards.
btrfs-patrol 0.5.0
Boot a snapshot from the boot menu, so a system that no longer starts can be rolled back without a live USB.
New
Set entries under [boot] in /etc/btrfs-patrol/config.toml and the newest snapshots each get an entry in the boot menu:
[boot]
entries = 5- Entries boot the snapshot read-only, so looking at a snapshot cannot change it.
- Roll back from inside a booted snapshot.
rollbackworks while running from one, which is the case that matters: the installed system is too broken to log into, so you boot a snapshot and roll back from there. - A booted snapshot is told apart from a pending rollback. They look similar from inside and mean opposite things;
checkandrollbacknow distinguish them and say which you are in.
Off by default (entries = 0): the boot menu is shared with the rest of the system.
Tested on real hardware, which changed how the entries are written — see the commit history for what that testing found.
btrfs-patrol 0.4.1
Hardening. No new commands — this release is about the ways the previous one could have gone wrong.
Fixed
- Never snapshot into an unmounted store. If the snapshots subvolume was not mounted, snapshots were written into the directory underneath it — onto the root subvolume, invisible to
list, and filling the disk they were meant to protect. It now refuses. - Never break the boot or break dnf. The dnf hook and the boot-entry writing are the two places where a failure here damages something else. Both now fail safe.
- Stop acting on a plan that stopped being true. A rollback plan is built from checks, then confirmed by a human, and the system can change in between. The plan is revalidated immediately before it executes, inside the store lock.
- Correctness and polish in terminal handling, subvolume names, device comparison, and
$BOOTresolution.
btrfs-patrol 0.4.0
Boot entries a rollback would otherwise strand.
New
btrfs-patrol prune-kernels removes the boot menu entries for kernels this system no longer has modules for.
Rolling back to an older snapshot restores that snapshot's /usr/lib/modules, but /boot is a separate partition and keeps every entry it had. The menu then offers kernels that cannot finish booting, because their modules went away with the rollback. prune-kernels finds those entries and removes them, with --dry-run to see the list first and a confirmation before anything is deleted.
Both kinds of entry are handled: /boot/loader/entries files and unified kernel images.
Entries kernel-install does not manage are left in place and reported, since kernel-install succeeds whether or not it removed anything. Rolling forward to a snapshot that did have those kernels needs kernel-install add-all to put their entries back.
btrfs-patrol 0.3.0
Snapshots of other subvolumes, such as /home.
New
btrfs-patrol can now manage subvolumes besides root. Add a table for each to /etc/btrfs-patrol/config.toml, then run sudo btrfs-patrol check:
[subvolumes.home]
path = "/home" # its mount point, or its path inside another subvolume
max_snapshots = 20 # default: retention.max_snapshots
timer = true # in the daily snapshot (default: true)
dnf = false # in dnf's snapshots too (default: false)- Snapshots:
snapshotand the daily timer take one snapshot of each subvolume, with an ID each.snapshot --subvolume hometakes only that one. dnf keeps snapshotting root only, unless a table saysdnf = true. - Listing and pruning:
listshows a SUBVOLUME column, the newsubvolume=homeselector picks one subvolume's snapshots, and each subvolume is pruned against its ownmax_snapshots. - Rollback, one subvolume at a time:
rollback IDrestores the subvolume that snapshot is of and nothing else. Undoing a bad update never throws away your documents, and restoring/homenever touches the system. The same safety as root's rollback: the current state is kept as arollbacksnapshot, nested subvolumes move along, and a failed step is undone.- A subvolume mounted on its own, like Fedora's
/home, has to be mounted bysubvol=in/etc/fstab, and is used from the next reboot. - A subvolume reached through its path inside another one, like
/var/logonce it is a subvolume, is replaced at once.
- A subvolume mounted on its own, like Fedora's
checkreports a table whose path isn't a btrfs subvolume on the same filesystem, warns about a subvolume inside root (root's snapshots and rollbacks don't include it), and about snapshots of subvolumes no longer in the configuration.
The man page has a new OTHER SUBVOLUMES section. The path in a table has to be a subvolume already; a helper to turn a directory into one is on the roadmap.
Compatibility
Nothing changes until you add a [subvolumes.<name>] table. Existing snapshots and configurations work as before. Snapshots of other subvolumes use metadata format 2, which names the subvolume; versions before 0.3 refuse to read it, so they can never roll root back to a snapshot of /home. Root snapshots keep format 1.
Tested
On a fresh Fedora 44 Workstation VM (GRUB, root found by subvol=root) and on a ThinkPad T480 (systemd-boot, root found through the default subvolume, LUKS and LVM), both with SELinux enforcing, upgraded from 0.2.2 and 0.2.1: snapshots of root and /home from the command and the timer, a rollback of /home that brought back a deleted file while root stayed untouched, and a rollback of root that left /home untouched, each with a reboot and no SELinux denials. 162 unit tests, which also run in the RPM build.
Upgrade
sudo dnf upgrade btrfs-patrol # from COPR
sudo dnf upgrade ./btrfs-patrol-0.3.0-1.fc44.noarch.rpm # or from this release
sudo btrfs-patrol checkbtrfs-patrol 0.2.2
Bug-fix release.
Fixed
-
No false warning about GRUB's rescue entry. Fedora's installer adds a GRUB rescue entry whose version is
0-rescue-<machine-id>. It names no kernel, so no snapshot has modules for it, yetrollbackcounted it and warned on every stock Fedora Workstation install:warning: snapshot 3 has no modules for 0-rescue-0613f4a2...; choose 6.19.10-300.fc44.x86_64 in the boot menu when you rebootBoot entries now have to name a version that looks like
uname -r, as unified kernel images already did. The rollback itself was never affected.
Tested
A fresh Fedora 44 Workstation install with its default layout (btrfs root and home subvolumes, GRUB, SELinux enforcing), installed from COPR as a new user would: dnf install btrfs-patrol pulls in libdnf5-plugin-actions, setup, manual, dnf and timer snapshots, then a rollback that removed a package installed after the snapshot and a roll forward that brought it back, each with a reboot and no SELinux denials. This was the first test on GRUB, where the root is found with rootflags=subvol=root rather than the default subvolume.
Upgrade
sudo dnf upgrade btrfs-patrol # from COPR
sudo dnf upgrade ./btrfs-patrol-0.2.2-1.fc44.noarch.rpm # or from this release
sudo btrfs-patrol check126 unit tests, which also run in the RPM build.
btrfs-patrol 0.2.1
Packaging release, and btrfs-patrol is now in COPR.
Install from COPR
btrfs-patrol is in the jccl1706/btrfs-patrol COPR repository, for Fedora 43 and newer on x86_64 and aarch64:
sudo dnf copr enable jccl1706/btrfs-patrol
sudo dnf install btrfs-patrol
sudo btrfs-patrol setupdnf upgrade then keeps it up to date.
Fixed
- The daily timer is restarted after an upgrade. The spec used
%systemd_postun, which expands to nothing in currentsystemd-rpm-macros(rpmlint:empty-%postun). It now uses%systemd_postun_with_restart, so when the timer is running, an upgrade restarts it at the end of the transaction and a changed schedule takes effect without a reboot.Persistent=truekeeps the restart from skipping a day. The restart runs from the package being upgraded from, so it takes effect from the upgrade after this one.
No changes to the program itself.
Upgrade
sudo dnf upgrade btrfs-patrol # from COPR
sudo dnf upgrade ./btrfs-patrol-0.2.1-1.fc44.noarch.rpm # or from this release
sudo btrfs-patrol check126 unit tests, which also run in the RPM build. See the 0.2.0 release for unified kernel image support and the manual page.