feat(btrbk): make snapshots explicit, guarded, and stored in @snapshots (#271)

* feat(btrbk): make snapshots explicit and guarded

Replace the always-on, hardcoded module with modules.btrbk options (enable/volume/subvolume/snapshotDir/target/onCalendar), and assert the btrfs top-level subvolume is mounted so a missing mount fails evaluation instead of silently failing at runtime.

Store snapshots in the @snapshots subvolume (snapshot_dir=@snapshots) rather than next to the live subvolume, make the off-host target explicit and off by default, and document that local snapshots are not off-host backups.

Enable the feature explicitly on idols-ai, shoukei and the three kubevirt hosts; qemu-guest now disables it through the option. Add eval tests for shoukei and ai.

* feat(monitoring): alert on btrbk snapshot failure

Add HostBtrbkSnapshotFailed on node_systemd_unit_state for btrbk-btrbk.service, so a broken snapshot schedule alerts instead of only surfacing in the journal.
This commit is contained in:
ryan4yin | 二花
2026-09-19 11:43:11 +08:00
committed by GitHub
parent e7f984745b
commit f8d12ada24
12 changed files with 166 additions and 33 deletions
+8
View File
@@ -29,6 +29,14 @@ in
inherit (myvars.networking) nameservers; inherit (myvars.networking) nameservers;
}; };
fileSystems."/btr_pool" = {
device = "/dev/disk/by-uuid/c2e8b249-240e-4eef-bf4e-81e7dbbf4887";
fsType = "btrfs";
options = [ "subvolid=5" ];
};
modules.btrbk.enable = true;
# This value determines the NixOS release from which the default # This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions # settings for stateful data, like file locations and database versions
# on your system were taken. Its perfectly fine and recommended to leave # on your system were taken. Its perfectly fine and recommended to leave
+2
View File
@@ -59,6 +59,8 @@ in
services.sunshine.enable = true; services.sunshine.enable = true;
services.tuned.ppdSettings.main.default = lib.mkForce "performance"; services.tuned.ppdSettings.main.default = lib.mkForce "performance";
modules.btrbk.enable = true;
powerManagement.resumeCommands = '' powerManagement.resumeCommands = ''
# Insta360 Link may stay enumerated with a stalled UVC endpoint after S3 resume. # Insta360 Link may stay enumerated with a stalled UVC endpoint after S3 resume.
${pkgs.coreutils}/bin/sleep 1 ${pkgs.coreutils}/bin/sleep 1
@@ -293,6 +293,17 @@ groups:
summary: Host systemd service crashed (instance {{ $labels.instance }}) summary: Host systemd service crashed (instance {{ $labels.instance }})
description: "systemd service crashed\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" description: "systemd service crashed\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: HostBtrbkSnapshotFailed
expr: 'node_systemd_unit_state{name="btrbk-btrbk.service",state="failed"} == 1'
for: 5m
labels:
severity: warning
annotations:
summary: Host btrbk snapshot/backup failed (instance {{ $labels.instance }})
description:
"btrbk-btrbk.service is in a failed state; btrfs snapshots are not being
created.\n VALUE = {{ $value }}\n LABELS = {{ $labels }}"
- alert: HostPhysicalComponentTooHot - alert: HostPhysicalComponentTooHot
expr: expr:
'((node_hwmon_temp_celsius * ignoring(label) group_left(instance, job, node, sensor) '((node_hwmon_temp_celsius * ignoring(label) group_left(instance, job, node, sensor)
+2
View File
@@ -57,6 +57,8 @@ in
k3sModule k3sModule
]; ];
modules.btrbk.enable = true;
boot.kernelParams = [ boot.kernelParams = [
# disable transparent hugepage(allocate hugepages dynamically) # disable transparent hugepage(allocate hugepages dynamically)
"transparent_hugepage=never" "transparent_hugepage=never"
+2
View File
@@ -51,6 +51,8 @@ in
k3sModule k3sModule
]; ];
modules.btrbk.enable = true;
boot.kernelParams = [ boot.kernelParams = [
# disable transparent hugepage(allocate hugepages dynamically) # disable transparent hugepage(allocate hugepages dynamically)
"transparent_hugepage=never" "transparent_hugepage=never"
+2
View File
@@ -51,6 +51,8 @@ in
k3sModule k3sModule
]; ];
modules.btrbk.enable = true;
boot.kernelParams = [ boot.kernelParams = [
# disable transparent hugepage(allocate hugepages dynamically) # disable transparent hugepage(allocate hugepages dynamically)
"transparent_hugepage=never" "transparent_hugepage=never"
+81 -25
View File
@@ -1,47 +1,103 @@
{
config,
lib,
...
}:
let
cfg = config.modules.btrbk;
in
{ {
# ================================================================== # ==================================================================
# #
# Tool for creating snapshots and remote backups of btrfs subvolumes # btrbk - scheduled LOCAL btrfs snapshots.
# https://github.com/digint/btrbk # https://github.com/digint/btrbk
# #
# Usage: # Snapshots are created in `snapshotDir` (relative to the btrfs top-level
# 1. btrbk will create snapshots on schedule # subvolume mounted at `volume`) and pruned by `snapshot_preserve`. With the
# 2. we can use `btrbk run` command to create a backup manually # defaults they land in the @snapshots subvolume (mounted at /snapshots):
# #
# How to restore a snapshot: # /btr_pool/@snapshots/@persistent.<timestamp>
# 1. Find the snapshot you want to restore in /snapshots #
# 2. Use `btrfs subvol delete /btr_pool/@persistent` to delete the current subvolume # These are same-filesystem snapshots: they protect against accidental
# 3. Use `btrfs subvol snapshot /snapshots/2021-01-01 /btr_pool/@persistent` to restore the snapshot # deletion and bad edits, NOT against disk loss. Set `target` (plus
# 4. reboot the system or remount the filesystem to see the changes # `services.btrbk.sshAccess` on the receiving host) for off-host backups.
#
# The host MUST mount the btrfs top-level subvolume (subvolid=5) at `volume`;
# this is enforced by an assertion so a missing mount fails evaluation instead
# of failing silently at 3am.
#
# Restore a snapshot (offline; stop writers first):
# 1. btrfs subvolume delete /btr_pool/@persistent
# 2. btrfs subvolume snapshot /btr_pool/@snapshots/@persistent.<timestamp> \
# /btr_pool/@persistent
# 3. reboot, or remount /persistent, to pick up the restored subvolume.
# #
# ================================================================== # ==================================================================
options.modules.btrbk = {
enable = lib.mkEnableOption "scheduled btrfs snapshots via btrbk";
volume = lib.mkOption {
type = lib.types.str;
default = "/btr_pool";
description = "Mount point of the btrfs top-level subvolume (subvolid=5).";
};
subvolume = lib.mkOption {
type = lib.types.str;
default = "@persistent";
description = "Source subvolume to snapshot, relative to {option}`modules.btrbk.volume`.";
};
snapshotDir = lib.mkOption {
type = lib.types.str;
default = "@snapshots";
description = "Directory the snapshots are created in, relative to {option}`modules.btrbk.volume`.";
};
target = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Optional off-host btrbk backup target (`target` in btrbk.conf). When
null, only local snapshots are kept, which does not protect against
disk loss.
'';
};
onCalendar = lib.mkOption {
type = lib.types.str;
default = "Tue,Sat *-*-* 3:45:20";
description = "systemd calendar expression for the snapshot timer.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = builtins.hasAttr cfg.volume config.fileSystems;
message = "modules.btrbk: no filesystem is mounted at `${cfg.volume}`; mount the btrfs top-level subvolume (subvolid=5) there so btrbk can snapshot `${cfg.volume}/${cfg.subvolume}`.";
}
];
services.btrbk.instances.btrbk = { services.btrbk.instances.btrbk = {
# How often this btrbk instance is started. See systemd.time(7) for more information about the format. onCalendar = cfg.onCalendar;
onCalendar = "Tue,Sat *-*-* 3:45:20";
settings = { settings = {
# how to prune local snapshots: # keep daily snapshots for 7 days, and always keep 2 days worth.
# 1. keep daily snapshots for xx days
snapshot_preserve = "7d"; snapshot_preserve = "7d";
# 2. keep all snapshots for 2 days, no matter how frequently you (or your cron job) run btrbk
snapshot_preserve_min = "2d"; snapshot_preserve_min = "2d";
# hot to prune remote incremental baqckups: # retention for an optional off-host target.
# keep daily backups for 9 days, weekly backups for 4 weeks, and monthly backups for 2 months
target_preserve = "9d 4w 2m"; target_preserve = "9d 4w 2m";
target_preserve_min = "no"; target_preserve_min = "no";
volume = { volume.${cfg.volume} = {
"/btr_pool" = { snapshot_dir = cfg.snapshotDir;
subvolume = { subvolume.${cfg.subvolume} = {
"@persistent" = {
snapshot_create = "always"; snapshot_create = "always";
}; };
}; }
// lib.optionalAttrs (cfg.target != null) {
# backup to a remote server or a local directory target = cfg.target;
# its prune policy is defined by `target_preserve` and `target_preserve_min`
# target = "/snapshots";
}; };
}; };
}; };
+1 -1
View File
@@ -20,7 +20,7 @@
config = { config = {
# disable backups in the VM # disable backups in the VM
services.btrbk.instances = lib.mkForce { }; modules.btrbk.enable = lib.mkForce false;
boot.growPartition = true; boot.growPartition = true;
boot.kernelParams = [ "console=ttyS0" ]; boot.kernelParams = [ "console=ttyS0" ];
@@ -0,0 +1,10 @@
{ ... }:
{
enabled = true;
hasBtrPool = true;
onCalendar = "Tue,Sat *-*-* 3:45:20";
snapshotDir = "@snapshots";
sources = [ "@persistent" ];
snapshotCreate = "always";
hasTarget = false;
}
@@ -0,0 +1,15 @@
{ outputs, ... }:
let
cfg = outputs.nixosConfigurations.shoukei-niri.config;
instance = cfg.services.btrbk.instances.btrbk;
volume = instance.settings.volume."/btr_pool";
in
{
enabled = cfg.modules.btrbk.enable;
hasBtrPool = cfg.fileSystems ? "/btr_pool";
onCalendar = instance.onCalendar;
snapshotDir = volume.snapshot_dir;
sources = builtins.attrNames volume.subvolume;
snapshotCreate = volume.subvolume."@persistent".snapshot_create;
hasTarget = volume ? target;
}
@@ -0,0 +1,10 @@
{ ... }:
{
enabled = true;
hasBtrPool = true;
onCalendar = "Tue,Sat *-*-* 3:45:20";
snapshotDir = "@snapshots";
sources = [ "@persistent" ];
snapshotCreate = "always";
hasTarget = false;
}
+15
View File
@@ -0,0 +1,15 @@
{ outputs, ... }:
let
cfg = outputs.nixosConfigurations.ai-niri.config;
instance = cfg.services.btrbk.instances.btrbk;
volume = instance.settings.volume."/btr_pool";
in
{
enabled = cfg.modules.btrbk.enable;
hasBtrPool = cfg.fileSystems ? "/btr_pool";
onCalendar = instance.onCalendar;
snapshotDir = volume.snapshot_dir;
sources = builtins.attrNames volume.subvolume;
snapshotCreate = volume.subvolume."@persistent".snapshot_create;
hasTarget = volume ? target;
}