nixos-config/modules/nixos/restic.nix

155 lines
4.6 KiB
Nix

# TODO: https://github.com/lilyinstarlight/foosteros/blob/dfe1ab3eb68bfebfaa709482d52fa04ebdde81c8/config/restic.nix#L23 <- this is better
{
config,
pkgs,
lib,
...
}:
let
inherit (lib)
mkEnableOption
mkOption
mkDefault
mkIf
types
getExe
;
cfg = config.custom.restic;
mapBtrfsRoots =
rootDir:
let
backupDir = lib.removeSuffix "/" "/backup${rootDir}";
slash = if rootDir == "/" then "" else "/";
awk = getExe pkgs.gawk;
continueIfInExclude = ''
exclude_subv="${toString cfg.btrfsExcludeSubvolume}"
found=false
for subv in $exclude_subv; do
if [[ "$subvol" == "$subv" ]]; then
found=true
echo "$subvol is in exclude subvolumes, skipped"
break
fi
done
$found && continue
'';
in
{
backupPrepareCommand = ''
echo "Creating snapshot for ${rootDir}"
subvolumes=$(${pkgs.btrfs-progs}/bin/btrfs subvolume list -o "${rootDir}" | ${awk} '{print $NF}')
mkdir -p "${backupDir}"
${pkgs.btrfs-progs}/bin/btrfs subvolume snapshot -r "${rootDir}" "${backupDir}/rootDirectory"
for subvol in $subvolumes; do
${continueIfInExclude}
[[ /"$subvol" == "${backupDir}"* ]] && continue
snapshot_path=$(dirname "${backupDir}/$subvol")
mkdir -p "$snapshot_path"
echo "Creating snapshot for subvolume: $subvol at $snapshot_path"
${pkgs.btrfs-progs}/bin/btrfs subvolume snapshot -r "${rootDir}${slash}$subvol" "$snapshot_path"
done
'';
# Note that all the manually created snapshots under backupDir will also be cleaned
backupCleanupCommand = ''
# Only find snapshots under backup directory
subvolumes=$(${pkgs.btrfs-progs}/bin/btrfs subvolume list -s -o "${backupDir}" | ${awk} '{print $NF}')
for subvol in $subvolumes; do
echo "Removing snapshot for subvolume: $subvol"
${pkgs.btrfs-progs}/bin/btrfs subvolume delete "$subvol"
done
'';
};
btrfsFs = lib.attrsets.filterAttrs (
n: v: v.fsType == "btrfs" && ((isNull cfg.btrfsRoots) || (builtins.elem n cfg.btrfsRoots))
) config.fileSystems;
btrfsFsRoot = builtins.attrNames btrfsFs;
btrfsCommands = (map mapBtrfsRoots btrfsFsRoot);
in
{
options = {
custom.restic = {
enable = mkEnableOption "restic";
paths = mkOption {
type = types.listOf types.str;
default = [
"/home"
"/var/lib"
];
};
prune = mkEnableOption "auto prune remote restic repo";
btrfsRoots = mkOption {
type = types.nullOr (types.listOf types.str);
default = [ "/" ];
description = ''
Includeded btrfs roots. `null` means snapshot all btrfs filesystems under config.fileSystems.
'';
};
btrfsExcludeSubvolume = mkOption {
type = types.listOf types.str;
default = [
"nix"
"rootfs"
"swap"
"var/tmp"
];
example = lib.literalExpression ''
[ "var/tmp" "srv" ]
'';
};
backupPrepareCommand = mkOption {
type = types.listOf types.str;
};
backupCleanupCommand = mkOption {
type = types.listOf types.str;
};
};
};
config = mkIf cfg.enable {
services.restic.backups.${config.networking.hostName} = {
repositoryFile = config.sops.secrets."restic/repo_url".path;
passwordFile = config.sops.secrets."restic/repo_password".path;
exclude = [
"**/.cache"
"**/.local/share/Steam"
"**/.local/share/flatpak"
"**/.cargo"
"**/.rustup"
"**/node_modules"
"*.pyc"
"*.pyo"
"**/__pycache__"
"**/.virtualenvs"
"**/.venv"
# temp files / lock files
"*.sqlite-wal"
"*.sqlite-shm"
"*.db-wal"
"*.db-shm"
];
timerConfig = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
};
pruneOpts = mkIf cfg.prune [
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 75"
];
paths = mkDefault cfg.paths;
initialize = true;
backupPrepareCommand = lib.strings.concatLines cfg.backupPrepareCommand;
backupCleanupCommand = lib.strings.concatLines cfg.backupCleanupCommand;
};
custom.restic.backupPrepareCommand = map (x: x.backupPrepareCommand) btrfsCommands;
custom.restic.backupCleanupCommand = map (x: x.backupCleanupCommand) btrfsCommands;
};
}