trying out some new tool commands for easier immpermanence with bcache and my setup

This commit is contained in:
RingOfStorms (Joshua Bell) 2025-12-17 13:39:39 -06:00
parent 03487772ac
commit 1d0cb5e08f
6 changed files with 487 additions and 54 deletions

View file

@ -68,6 +68,7 @@
./hardware-configuration.nix ./hardware-configuration.nix
./hardware-mounts.nix ./hardware-mounts.nix
./impermanence.nix ./impermanence.nix
./impermanence-tools.nix
# ./preservation.nix # ./preservation.nix
( (
{ {

View file

@ -0,0 +1,67 @@
{ config, lib, pkgs, ... }:
let
cfg = config.impermanence.tools;
bcacheImpermanenceBin = pkgs.writeShellScriptBin "bcache-impermanence" (
builtins.readFile ./impermanence-tools.sh
);
in
{
options.impermanence.tools = {
enable = lib.mkEnableOption "bcachefs impermanence tools (GC + CLI)";
snapshotRoot = lib.mkOption {
type = lib.types.str;
default = "/.snapshots/old_roots";
description = "Root directory containing old root snapshots.";
};
gc = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable garbage collection of old root snapshots.";
};
keepPerMonth = lib.mkOption {
type = lib.types.int;
default = 1;
description = "Keep at least this many snapshots per calendar month (latest ones).";
};
keepRecentWeeks = lib.mkOption {
type = lib.types.int;
default = 4;
description = "Keep at least one snapshot per ISO week within this many recent weeks.";
};
keepRecentCount = lib.mkOption {
type = lib.types.int;
default = 5;
description = "Always keep at least this many most recent snapshots overall.";
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ bcacheImpermanenceBin ];
systemd.services."bcache-impermanence-gc" = lib.mkIf cfg.gc.enable {
description = "Garbage collect bcachefs impermanence snapshots";
wantedBy = [ "multi-user.target" ];
after = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
Environment = "PATH=${lib.makeBinPath [ pkgs.coreutils pkgs.findutils pkgs.diffutils pkgs.bcachefs-tools ]}:/run/current-system/sw/bin";
};
script = ''
exec ${bcacheImpermanenceBin}/bin/bcache-impermanence gc \
--snapshot-root ${cfg.snapshotRoot} \
--keep-per-month ${toString cfg.gc.keepPerMonth} \
--keep-recent-weeks ${toString cfg.gc.keepRecentWeeks} \
--keep-recent-count ${toString cfg.gc.keepRecentCount}
'';
};
};
}

View file

@ -0,0 +1,379 @@
#!/usr/bin/env bash
set -eu
SNAPSHOT_ROOT="/.snapshots/old_roots"
KEEP_PER_MONTH=1
KEEP_RECENT_WEEKS=4
KEEP_RECENT_COUNT=5
DRY_RUN=0
usage() {
cat <<EOF
bcache-impermanence - tools for managing impermanence snapshots
Usage:
bcache-impermanence gc [--snapshot-root DIR] [--keep-per-month N] [--keep-recent-weeks N] [--keep-recent-count N] [--dry-run]
bcache-impermanence ls [-n1] [--snapshot-root DIR]
bcache-impermanence diff [-s SNAPSHOT] [--snapshot-root DIR] [PATH_PREFIX...]
Subcommands:
gc Run garbage collection on old root snapshots.
ls List snapshots (newest first). With -n1 prints only latest.
diff Show diff between current system and a snapshot.
Options:
--snapshot-root DIR Override snapshot root directory (default: /.snapshots/old_roots).
--keep-per-month N For gc: keep at least N snapshots per calendar month.
--keep-recent-weeks N For gc: keep at least one snapshot per ISO week within the last N weeks.
--keep-recent-count N For gc: always keep at least N most recent snapshots overall.
--dry-run For gc: show what would be deleted.
EOF
}
ensure_deps() {
for cmd in date sort basename diff bcachefs; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "Missing required command: $cmd" >&2
exit 1
fi
done
}
list_snapshots_desc() {
if [ ! -d "$SNAPSHOT_ROOT" ]; then
return 0
fi
for entry in "$SNAPSHOT_ROOT"/*; do
[ -d "$entry" ] || continue
basename "$entry"
done | sort -r
}
latest_snapshot_name() {
list_snapshots_desc | head -n1
}
cmd_ls() {
local n1=0
while [ "$#" -gt 0 ]; do
case "$1" in
-n1)
n1=1
;;
--snapshot-root)
shift
[ "$#" -gt 0 ] || { echo "--snapshot-root requires a value" >&2; exit 1; }
SNAPSHOT_ROOT="$1"
;;
--help|-h)
echo "Usage: bcache-impermanence ls [-n1] [--snapshot-root DIR]" >&2
exit 0
;;
*)
echo "Unknown ls option: $1" >&2
exit 1
;;
esac
shift
done
local snaps
snaps=$(list_snapshots_desc)
if [ -z "$snaps" ]; then
echo "No snapshots found in $SNAPSHOT_ROOT" >&2
exit 1
fi
if [ "$n1" -eq 1 ]; then
printf '%s
' "$snaps" | head -n1
else
printf '%s
' "$snaps"
fi
}
build_keep_set() {
# Prints snapshot names to keep, one per line, based on policies.
local now
now=$(date +%s)
local snaps
snaps=$(list_snapshots_desc)
if [ -z "$snaps" ]; then
return 0
fi
local tmpdir
tmpdir=$(mktemp -d)
# Always keep newest KEEP_RECENT_COUNT snapshots.
if [ "$KEEP_RECENT_COUNT" -gt 0 ]; then
printf '%s
' "$snaps" | head -n "$KEEP_RECENT_COUNT" >"$tmpdir/keep_recent"
fi
# Per-month: keep up to KEEP_PER_MONTH newest per month.
if [ "$KEEP_PER_MONTH" -gt 0 ]; then
# Iterate newest -> oldest.
while read -r snap; do
[ -n "$snap" ] || continue
local month
month=${snap%_*} # YYYY-MM-DD
month=${month%-*} # YYYY-MM
local month_file="$tmpdir/month_$month"
local count=0
if [ -f "$month_file" ]; then
count=$(wc -l <"$month_file")
fi
if [ "$count" -lt "$KEEP_PER_MONTH" ]; then
echo "$snap" >>"$month_file"
fi
done <<EOF_SNAPS
$snaps
EOF_SNAPS
fi
# Recent weeks: keep latest snapshot per week within last KEEP_RECENT_WEEKS weeks.
if [ "$KEEP_RECENT_WEEKS" -gt 0 ]; then
local max_age
max_age=$(( KEEP_RECENT_WEEKS * 7 * 24 * 3600 ))
while read -r snap; do
[ -n "$snap" ] || continue
local ts
ts=$(date -d "${snap%_*} ${snap#*_}" +%s 2>/dev/null || true)
[ -n "$ts" ] || continue
local age
age=$(( now - ts ))
if [ "$age" -gt "$max_age" ]; then
continue
fi
local week
week=$(date -d "${snap%_*} ${snap#*_}" +"%G-%V" 2>/dev/null || true)
[ -n "$week" ] || continue
local week_file="$tmpdir/week_$week"
if [ ! -f "$week_file" ]; then
echo "$snap" >"$week_file"
fi
done <<EOF_SNAPS2
$snaps
EOF_SNAPS2
fi
# Aggregate and dedupe.
for f in "$tmpdir"/*; do
[ -f "$f" ] || continue
cat "$f"
done | sort -u
rm -rf "$tmpdir"
}
cmd_gc() {
while [ "$#" -gt 0 ]; do
case "$1" in
--snapshot-root)
shift
[ "$#" -gt 0 ] || { echo "--snapshot-root requires a value" >&2; exit 1; }
SNAPSHOT_ROOT="$1"
;;
--keep-per-month)
shift
[ "$#" -gt 0 ] || { echo "--keep-per-month requires a value" >&2; exit 1; }
KEEP_PER_MONTH="$1"
;;
--keep-recent-weeks)
shift
[ "$#" -gt 0 ] || { echo "--keep-recent-weeks requires a value" >&2; exit 1; }
KEEP_RECENT_WEEKS="$1"
;;
--keep-recent-count)
shift
[ "$#" -gt 0 ] || { echo "--keep-recent-count requires a value" >&2; exit 1; }
KEEP_RECENT_COUNT="$1"
;;
--dry-run)
DRY_RUN=1
;;
--help|-h)
echo "Usage: bcache-impermanence gc [--snapshot-root DIR] [--keep-per-month N] [--keep-recent-weeks N] [--keep-recent-count N] [--dry-run]" >&2
exit 0
;;
*)
echo "Unknown gc option: $1" >&2
exit 1
;;
esac
shift
done
if [ ! -d "$SNAPSHOT_ROOT" ]; then
echo "Snapshot root $SNAPSHOT_ROOT does not exist; nothing to do" >&2
exit 0
fi
local snaps
snaps=$(list_snapshots_desc)
if [ -z "$snaps" ]; then
echo "No snapshots to process" >&2
exit 0
fi
local keep
keep=$(build_keep_set)
local tmpkeep
tmpkeep=$(mktemp -d)
while read -r k; do
[ -n "$k" ] || continue
: >"$tmpkeep/$k"
done <<EOF_KEEP
$keep
EOF_KEEP
local deleted=0
while read -r snap; do
[ -n "$snap" ] || continue
if [ -f "$tmpkeep/$snap" ]; then
continue
fi
local full
full="$SNAPSHOT_ROOT/$snap"
if [ "$DRY_RUN" -eq 1 ]; then
echo "[dry-run] Would delete $full"
else
echo "Deleting snapshot $full"
if ! bcachefs subvolume delete "$full"; then
echo "Failed to delete $full" >&2
else
deleted=$((deleted + 1))
fi
fi
done <<EOF_SNAPS
$snaps
EOF_SNAPS
rm -rf "$tmpkeep"
echo "GC complete; deleted $deleted snapshots"
}
cmd_diff() {
local snapshot_name=""
while [ "$#" -gt 0 ]; do
case "$1" in
-s)
shift
[ "$#" -gt 0 ] || { echo "-s requires a snapshot name" >&2; exit 1; }
snapshot_name="$1"
;;
--snapshot-root)
shift
[ "$#" -gt 0 ] || { echo "--snapshot-root requires a value" >&2; exit 1; }
SNAPSHOT_ROOT="$1"
;;
--help|-h)
echo "Usage: bcache-impermanence diff [-s SNAPSHOT] [--snapshot-root DIR] [PATH_PREFIX...]" >&2
exit 0
;;
--*)
echo "Unknown diff option: $1" >&2
exit 1
;;
*)
break
;;
esac
shift
done
if [ -z "$snapshot_name" ]; then
snapshot_name=$(latest_snapshot_name || true)
fi
if [ -z "$snapshot_name" ]; then
echo "No snapshots found for diff" >&2
exit 1
fi
local snapshot_dir
snapshot_dir="$SNAPSHOT_ROOT/$snapshot_name"
if [ ! -d "$snapshot_dir" ]; then
echo "Snapshot directory $snapshot_dir does not exist" >&2
exit 1
fi
if [ "$#" -eq 0 ]; then
set -- /
fi
local rc=0
while [ "$#" -gt 0 ]; do
local path
path="$1"
shift
case "$path" in
/*) : ;;
*)
echo "Path prefix must be absolute: $path" >&2
rc=2
continue
;;
esac
local from
local to
from="$snapshot_dir$path"
to="$path"
echo "--- Diff for $path (snapshot $snapshot_name) ---"
if ! diff -ru --new-file "$from" "$to"; then
local diff_rc=$?
if [ "$diff_rc" -gt 1 ]; then
echo "Error running diff for $path" >&2
rc=$diff_rc
fi
fi
done
exit "$rc"
}
main() {
if [ "$#" -lt 1 ]; then
usage
exit 1
fi
ensure_deps
local cmd
cmd="$1"
shift || true
case "$cmd" in
gc)
cmd_gc "$@"
;;
ls)
cmd_ls "$@"
;;
diff)
cmd_diff "$@"
;;
--help|-h|help)
usage
;;
*)
echo "Unknown subcommand: $cmd" >&2
usage
exit 1
;;
esac
}
main "$@"

68
hosts/lio/flake.lock generated
View file

@ -31,11 +31,11 @@
}, },
"locked": { "locked": {
"dir": "flakes/beszel", "dir": "flakes/beszel",
"lastModified": 1765815414, "lastModified": 1765998800,
"narHash": "sha256-PVwVlVPMRPqVpd6G65u4VyGJWJL7JtcgX1r1NtfpWYE=", "narHash": "sha256-tFJsYcZQMuin4gtqjPpdg4V4QlCvpzLQ0H1ct5LM9Rg=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "d4088ffaa8039f024dd851146b5b7b34c6253257", "rev": "03487772acd9e0865207baf281f7a0478f6dbc16",
"revCount": 912, "revCount": 944,
"type": "git", "type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" "url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}, },
@ -149,11 +149,11 @@
}, },
"locked": { "locked": {
"dir": "flakes/flatpaks", "dir": "flakes/flatpaks",
"lastModified": 1765815414, "lastModified": 1765998800,
"narHash": "sha256-PVwVlVPMRPqVpd6G65u4VyGJWJL7JtcgX1r1NtfpWYE=", "narHash": "sha256-tFJsYcZQMuin4gtqjPpdg4V4QlCvpzLQ0H1ct5LM9Rg=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "d4088ffaa8039f024dd851146b5b7b34c6253257", "rev": "03487772acd9e0865207baf281f7a0478f6dbc16",
"revCount": 912, "revCount": 944,
"type": "git", "type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" "url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}, },
@ -190,11 +190,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1765605144, "lastModified": 1765979862,
"narHash": "sha256-RM2xs+1HdHxesjOelxoA3eSvXShC8pmBvtyTke4Ango=", "narHash": "sha256-/r9/1KamvbHJx6I40H4HsSXnEcBAkj46ZwibhBx9kg0=",
"owner": "rycee", "owner": "rycee",
"repo": "home-manager", "repo": "home-manager",
"rev": "90b62096f099b73043a747348c11dbfcfbdea949", "rev": "d3135ab747fd9dac250ffb90b4a7e80634eacbe9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -261,11 +261,11 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1765472234, "lastModified": 1765779637,
"narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=", "narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b", "rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -277,11 +277,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1764983851, "lastModified": 1765762245,
"narHash": "sha256-y7RPKl/jJ/KAP/VKLMghMgXTlvNIJMHKskl8/Uuar7o=", "narHash": "sha256-3iXM/zTqEskWtmZs3gqNiVtRTsEjYAedIaLL0mSBsrk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "d9bc5c7dceb30d8d6fafa10aeb6aa8a48c218454", "rev": "c8cfcd6ccd422e41cc631a0b73ed4d5a925c393d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -293,11 +293,11 @@
}, },
"nixpkgs_3": { "nixpkgs_3": {
"locked": { "locked": {
"lastModified": 1765762245, "lastModified": 1765838191,
"narHash": "sha256-3iXM/zTqEskWtmZs3gqNiVtRTsEjYAedIaLL0mSBsrk=", "narHash": "sha256-m5KWt1nOm76ILk/JSCxBM4MfK3rYY7Wq9/TZIIeGnT8=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c8cfcd6ccd422e41cc631a0b73ed4d5a925c393d", "rev": "c6f52ebd45e5925c188d1a20119978aa4ffd5ef6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -309,11 +309,11 @@
}, },
"nixpkgs_4": { "nixpkgs_4": {
"locked": { "locked": {
"lastModified": 1765644376, "lastModified": 1765934234,
"narHash": "sha256-yqHBL2wYGwjGL2GUF2w3tofWl8qO9tZEuI4wSqbCrtE=", "narHash": "sha256-pJjWUzNnjbIAMIc5gRFUuKCDQ9S1cuh3b2hKgA7Mc4A=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "23735a82a828372c4ef92c660864e82fbe2f5fbe", "rev": "af84f9d270d404c17699522fab95bbf928a2d92f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -1224,11 +1224,11 @@
}, },
"locked": { "locked": {
"dir": "flakes/opencode", "dir": "flakes/opencode",
"lastModified": 1765815414, "lastModified": 1765998800,
"narHash": "sha256-PVwVlVPMRPqVpd6G65u4VyGJWJL7JtcgX1r1NtfpWYE=", "narHash": "sha256-tFJsYcZQMuin4gtqjPpdg4V4QlCvpzLQ0H1ct5LM9Rg=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "d4088ffaa8039f024dd851146b5b7b34c6253257", "rev": "03487772acd9e0865207baf281f7a0478f6dbc16",
"revCount": 912, "revCount": 944,
"type": "git", "type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" "url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}, },
@ -1243,11 +1243,11 @@
"nixpkgs": "nixpkgs_4" "nixpkgs": "nixpkgs_4"
}, },
"locked": { "locked": {
"lastModified": 1765814475, "lastModified": 1765998786,
"narHash": "sha256-+HC5nvbpcrvTyWuJDLqC13KuDKMGwjmoYb2tm+hGDzQ=", "narHash": "sha256-OxSKJ9QR7demkOF6hrWaf4yS3fMGlU/FbF12VYsX5Mk=",
"owner": "sst", "owner": "sst",
"repo": "opencode", "repo": "opencode",
"rev": "56dde2cc835f509f77cbd800d080d6dbb2b8edc6", "rev": "1f527312554c3015286811fc33bb5348d0a27dae",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -1433,11 +1433,11 @@
}, },
"locked": { "locked": {
"dir": "flakes/secrets", "dir": "flakes/secrets",
"lastModified": 1765815414, "lastModified": 1765998800,
"narHash": "sha256-PVwVlVPMRPqVpd6G65u4VyGJWJL7JtcgX1r1NtfpWYE=", "narHash": "sha256-tFJsYcZQMuin4gtqjPpdg4V4QlCvpzLQ0H1ct5LM9Rg=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "d4088ffaa8039f024dd851146b5b7b34c6253257", "rev": "03487772acd9e0865207baf281f7a0478f6dbc16",
"revCount": 912, "revCount": 944,
"type": "git", "type": "git",
"url": "https://git.joshuabell.xyz/ringofstorms/dotfiles" "url": "https://git.joshuabell.xyz/ringofstorms/dotfiles"
}, },

View file

@ -148,6 +148,12 @@
common.homeManagerModules.starship common.homeManagerModules.starship
common.homeManagerModules.zoxide common.homeManagerModules.zoxide
common.homeManagerModules.zsh common.homeManagerModules.zsh
(
{ ... }:
{
programs.tmux.package = pkgs.unstable.tmux;
}
)
]; ];
extraSpecialArgs = { extraSpecialArgs = {

View file

@ -125,23 +125,3 @@ mount -t bcachefs --mkdir /dev/$DEVICE /usb_key
echo "test" > /usb_key/key echo "test" > /usb_key/key
umount /usb_key && rmdir /usb_key umount /usb_key && rmdir /usb_key
``` ```
## TODO remove notes
```sh
BOOT=sda1
PRIMARY=sda2
SWAP=sda3
swapon /dev/$SWAP
keyctl link @u @s
DEV_B="/dev/disk/by-uuid/"$(lsblk -o name,uuid | grep $BOOT | awk '{print $2}')
DEV_P="/dev/disk/by-uuid/"$(lsblk -o name,uuid | grep $PRIMARY | awk '{print $2}')
mount -t bcachefs -o X-mount.subdir=@root $DEV_P /mnt
mount -t vfat $DEV_B /mnt/boot --mkdir
mount -t bcachefs -o X-mount.mkdir,X-mount.subdir=@nix,relatime $DEV_P /mnt/nix
mount -t bcachefs -o X-mount.mkdir,X-mount.subdir=@snapshots,relatime $DEV_P /mnt/.snapshots
mount -t bcachefs -o X-mount.mkdir,X-mount.subdir=@persist $DEV_P /mnt/persist
nixos-install --flake "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=hosts/i001#i001"
```