Skip to content

Snapshot lifecycle: rolling history, retention, btrfs IDs#21

Merged
rocketman-code merged 10 commits intomainfrom
snapshot-lifecycle
Apr 14, 2026
Merged

Snapshot lifecycle: rolling history, retention, btrfs IDs#21
rocketman-code merged 10 commits intomainfrom
snapshot-lifecycle

Conversation

@rocketman-code
Copy link
Copy Markdown
Owner

Summary

  • Automatic snapshots use rolling timestamp names (%Y-%m-%d_%H-%M-%S) with configurable retention via MAX_SNAPSHOTS in /etc/atomic-rollback.conf (default 50). User-named snapshots never counted, never evicted.
  • snapshot list shows a three-column table: btrfs subvolume ID, name, creation time.
  • rollback [id|name] and snapshot delete <id|name> accept btrfs subvolume IDs in addition to names. rollback with no arguments defaults to the most recent snapshot (highest ID).
  • Kernel-install hook ownership transferred from RPM to migrate. The hook now persists across atomic-rollback uninstall, so removing the tool while on a migrated layout does not break future kernel upgrades.
  • libdnf5 actions plugin removed. The RPM C plugin (shipped in 0.3.7) covers all RPM-based frontends and made the libdnf5 plugin redundant.
  • check output uses "boot chain" terminology instead of "system bootable." The formal model verifies boot chain structural validity, not system bootability in the broader sense.
  • Systems upgrading from v0.3.x have their legacy root.pre-update snapshot renamed to its creation timestamp on upgrade, so it joins the rolling history and participates in retention instead of persisting indefinitely.
  • --version / -V print the package version.

Closes #9, #16, #17.

Test plan

  • 23/23 unit tests pass (cargo test --release)
  • VM (Fedora 43 btrfs): snapshot create / snapshot list / snapshot delete by name and by ID
  • VM (Fedora 43 btrfs): retention evicts oldest auto-named when MAX_SNAPSHOTS exceeded; user-named untouched
  • VM (Fedora 43 btrfs): root.pre-update rename preserves btrfs ID (327→327); idempotent silent no-op on re-run; renamed snapshot participates in retention
  • VM (Fedora 32 ext4): rename-legacy-snapshot silent no-op, exit 0, no mount attempts on non-btrfs
  • VM: --version, -V, captured via $() (stdout verified); rejects -v (lowercase), --VERSION (case), bare version; trailing args ignored; --help and check unaffected

The libdnf5 actions plugin and the RPM plugin both fire on dnf5
transactions, producing duplicate snapshot messages. The RPM plugin's
coverage strictly subsumes the libdnf5 plugin: it catches dnf5, dnf4,
pure rpm, PackageKit, and anything else that routes through librpm.
The libdnf5 plugin catches only dnf5.

This was a failed cleanup from v0.3.7 when the RPM plugin was added.

Remove the .actions file, the libdnf5-plugin-actions runtime dependency,
and all references to "the dnf plugin" in code comments, documentation,
and function doc comments.
The kernel-install hook (90-atomic-rollback.install) was owned by the
RPM package. If the package was uninstalled after migrate had permanently
changed the on-disk layout, the hook was removed but the migrated layout
persisted. Subsequent kernel installs produced BLS entries with paths
GRUB could not resolve, making new kernels unbootable.

The hook is now a migration artifact: migrate writes it to
/usr/lib/kernel/install.d/ alongside its other persistent changes
(fstab markers, root symlinks, ESP grub.cfg rewrites). The hook
persists after package removal because RPM no longer owns it.

Add internal ensure-hooks command (not in --help, same pattern as
kernel-hook) that checks migration state using the existing detection
logic (btrfs root + /boot not a separate mount) and writes the hook
if migrated and missing. %posttrans calls ensure-hooks to restore the
hook for existing migrated users upgrading to this version.

Fixes #17.
Replace the fixed-name idempotent snapshot with a uniquely-named
snapshot per invocation. The auto-generated name uses %Y-%m-%d_%H-%M-%S
in local time.

Each RPM transaction now creates a new snapshot instead of no-oping on
the existing root.pre-update. User-named snapshots (snapshot create
<name>) continue to work through the same code path.

DEFAULT_SNAPSHOT_NAME remains in consts.rs for rollback no-args default;
that dependency is removed in a later commit.
Add a keep-last-N retention policy for automatic snapshots. After
creating an auto-named snapshot, if the count of automatic snapshots
exceeds MAX_SNAPSHOTS, the oldest are evicted. User-named snapshots
are never counted and never evicted.

Automatic snapshots are identified by their name matching the
YYYY-MM-DD_HH-MM-SS format. User-supplied names matching this format
are rejected at creation time to prevent collision.

MAX_SNAPSHOTS defaults to 50 and is configurable in
/etc/atomic-rollback.conf using shell key=value format (derived from
snapper, openSUSE/snapper data/default-config).

Retention failure is non-fatal: errors are logged but the snapshot
creation succeeds and the RPM transaction is not blocked.
Thread the btrfs-assigned subvolume ID through SnapshotResult and the
delete return type so all user-facing messages display it. The ID is
looked up via the existing btrfs_subvol_id_by_name after creation, and
surfaced from delete which already looked it up internally.

Create:  Snapshot '<name>' with ID <id> created.
Existed: Snapshot '<name>' with ID <id> already exists.
Delete:  Snapshot '<name>' with ID <id> deleted.
Replace the bare-names list with a table showing btrfs subvolume ID,
filesystem name, and creation time. Sorted by ID ascending
(chronological order).

Creation time is read from btrfs subvolume show via a new
btrfs_subvol_creation_time helper in tools.rs, with the timezone
suffix stripped to produce local-time %Y-%m-%d %H:%M:%S output.

Column widths are computed from the data so the table adapts to
varying ID digit counts and name lengths.
Rollback and delete now accept btrfs subvolume IDs (numeric) in
addition to names. Rollback with no arguments targets the most recent
snapshot by highest btrfs subvolume ID instead of the removed
DEFAULT_SNAPSHOT_NAME constant.

Add resolve_snapshot and most_recent_snapshot to snapshot.rs, both
pure composition on the existing btrfs_subvol_list output.

Remove DEFAULT_SNAPSHOT_NAME from consts.rs (zero consumers remain).
Replace all user-facing output, doc comments, and documentation
instances of "bootable" and "system is bootable" with "boot chain"
language that matches what the formal model actually verifies. The
tool verifies boot chain structural validity (ESP, GRUB, BLS entries,
root mount), not system bootability in the broader sense (kernel
bugs, runtime failures, etc. are outside the verification scope).

Internal function names (verify_bootable, BootStatus) and formal
model terminology (proof.rs theorem names, BOOTS predicate) are
unchanged. These are internal API and the formal model's own domain
language, not user-facing claims.
Systems that had atomic-rollback v0.3.x installed carry a
`root.pre-update` subvolume created by the old fixed-name plugin. On
upgrade to the rolling-history naming, this subvolume survives but is
treated as user-named: it appears in snapshot list alongside
timestamped entries with an inconsistent name, and retention never
touches it (is_auto_name rejects it, so it is preserved indefinitely).

Add a one-shot migration that renames root.pre-update to its creation
timestamp formatted as YYYY-MM-DD_HH-MM-SS (matching the auto-name
format). The btrfs subvolume ID is preserved across the rename, so
rollback behavior for that snapshot is unchanged.

The migration is invoked from %posttrans via a hidden
rename-legacy-snapshot subcommand, matching the ensure-hooks pattern.
Idempotent no-op if root.pre-update is absent (fresh install, already
migrated, non-btrfs root).
Print the package version so users and scripts can query it without
going through rpm -q. Output format: `atomic-rollback v<version>`,
matching btrfs-progs style. Single line on stdout, exit 0.

Both --version and -V are accepted. -v (lowercase) is deliberately
not matched, keeping it free for a potential future --verbose flag.

Version string resolves from env!("CARGO_PKG_VERSION") at compile
time, so it always reflects the Cargo.toml version baked into the
binary.
@rocketman-code rocketman-code merged commit 665f675 into main Apr 14, 2026
2 checks passed
@rocketman-code rocketman-code deleted the snapshot-lifecycle branch April 14, 2026 06:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Include timestamp in name of automatic snapshot

1 participant