aboutsummaryrefslogtreecommitdiffstats
path: root/modules/by-name/ba/backup/module.nix
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--modules/by-name/ba/backup/module.nix216
1 files changed, 177 insertions, 39 deletions
diff --git a/modules/by-name/ba/backup/module.nix b/modules/by-name/ba/backup/module.nix
index 3e07fbd1..e601d840 100644
--- a/modules/by-name/ba/backup/module.nix
+++ b/modules/by-name/ba/backup/module.nix
@@ -11,92 +11,230 @@
lib,
config,
pkgs,
+ libraries,
...
}: let
cfg = config.soispha.services.backup;
+
+ snapshotDir = "/srv/last_snapshot";
in {
options.soispha.services.backup = {
- enable = lib.mkEnableOption "backups via restic to a storagebox";
+ enable = libraries.base.options.mkEnable "backups via restic";
- user = lib.mkOption {
- type = lib.types.str;
- description = "The storagebox-user to use";
- example = "u384702-sub2";
- };
- privateSshKey = lib.mkOption {
- type = lib.types.path;
- description = "The age-encrypted ssh-key, passed to agenix";
+ storagebox = {
+ enable = lib.mkEnableOption "remote backups";
+ user = lib.mkOption {
+ type = lib.types.str;
+ description = "The storagebox-user to use";
+ example = "u384702-sub2";
+ };
+
+ sshKey = lib.mkOption {
+ type = lib.types.path;
+ description = "The age-encrypted ssh-key, passed to agenix";
+ default = ./secrets/storagebox/ssh_key.age;
+ };
+
+ repositoryPassword = lib.mkOption {
+ type = lib.types.path;
+ description = "The age-encrypted restic password, passed to agenix";
+ default = ./secrets/storagebox/repository_password.age;
+ };
};
- privatePassword = lib.mkOption {
- type = lib.types.path;
- description = "The age-encrypted restic password, passed to agenix";
+
+ local = {
+ enable = lib.mkEnableOption "local backups";
+
+ repositoryPassword = lib.mkOption {
+ type = lib.types.path;
+ description = "The age-encrypted restic password, passed to agenix";
+ default = ./secrets/local/repository_password.age;
+ };
+
+ backupMountPoint = lib.mkOption {
+ type = lib.types.path;
+ description = "The path where to expect the mounted backup disk";
+ default = "/mnt/backup";
+ };
};
};
config = lib.mkIf cfg.enable {
+ soispha.impermanence.directories = lib.mkMerge [
+ (lib.mkIf cfg.storagebox.enable [
+ "/var/cache/restic-backups-storagebox"
+
+ # Generate the base systemd.mount from impermanence (we will override this, so it
+ # points to the non-admin one instead)
+ "/var/cache/restic-backups-storagebox-admin"
+ ])
+ (lib.mkIf cfg.local.enable [
+ "/var/cache/restic-backups-local"
+ ])
+ ];
+
+ # TODO: Make this work (`systemd.mounts` is a list, so _this_ doesn't work.) <2026-07-07>
+ # systemd.mounts."/var/cache/restic-backups-storagebox-admin" = {
+ # # TODO: Don't hardcode the path, but use
+ # # `systemd.mounts."/var/cache/restic-backups-storagebox".what` instead <2026-07-07>
+ # what = "/srv/var/cache/restic-backups-storagebox";
+ # };
+
age.secrets = {
- resticpass = {
- file = cfg.privatePassword;
+ resticStorageboxSshKey = lib.mkIf cfg.storagebox.enable {
+ file = cfg.storagebox.sshKey;
mode = "0700";
owner = "root";
group = "root";
};
- resticssh = {
- file = cfg.privateSshKey;
+ resticStorageboxRepositoryPassword = lib.mkIf cfg.storagebox.enable {
+ file = cfg.storagebox.repositoryPassword;
+ mode = "0700";
+ owner = "root";
+ group = "root";
+ };
+ resticLocalRepositoryPassword = lib.mkIf cfg.local.enable {
+ file = cfg.local.repositoryPassword;
mode = "0700";
owner = "root";
group = "root";
};
};
- soispha.programs.ssh = {
+ soispha.programs.ssh = lib.mkIf cfg.storagebox.enable {
enable = true;
rootKnownHosts = {
"[u459143-sub1.your-storagebox.de]:23" = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIICf9svRenC/PLKIL9nk6K/pxQgoiFC41wTNvoIncOxs";
};
};
+ programs.fuse.enable = cfg.storagebox.enable || cfg.local.enable;
+
+ systemd.services = {
+ prepare-backup = {
+ requires = [];
+ after = [];
+
+ description = "Prepare a backup by snapshotting the system.";
+
+ serviceConfig = {
+ ExecStart = lib.getExe (pkgs.writeShellApplication {
+ name = "prepareBackup";
+ text = ''
+ set -x
+
+ [ -d "${snapshotDir}" ] && btrfs subvolume delete "${snapshotDir}"
+
+ # -r := Make the snapshot read-only
+ btrfs subvolume snapshot -r /srv "${snapshotDir}";
+ '';
+
+ inheritPath = false;
+ runtimeInputs = [
+ pkgs.btrfs-progs
+ ];
+ });
+
+ Type = "oneshot";
+
+ User = "root";
+ Group = "root";
+
+ # TODO: Hardening <2025-05-04>
+ };
+ };
+
+ restic-backups-storagebox = lib.mkIf cfg.storagebox.enable {
+ requires = ["prepare-backup.service"];
+ after = ["prepare-backup.service"];
+ };
+
+ restic-backups-local = lib.mkIf cfg.local.enable {
+ requires = ["prepare-backup.service"];
+ after = ["prepare-backup.service"];
+
+ serviceConfig = {
+ ConditionPathIsDirectory = "${cfg.local.backupMountPoint}";
+ };
+ };
+ };
+
services.restic.backups = let
- snapshotDir = "/srv/last_snapshot";
homeDir = "${snapshotDir}/home";
+
+ paths = [
+ snapshotDir
+ ];
+ exclude = [
+ "${homeDir}/soispha/.cache"
+ "${homeDir}/soispha/.local/share/lutris/install"
+ "${homeDir}/soispha/.local/share/Steam/steamapps/common"
+ ];
+ extraBackupArgs = [
+ # Don't scan the filesystem to determine an estimate.
+ "--no-scan"
+ "--verbose=2"
+ ];
+ environmentFile = "${./env.vars}";
in {
- storagebox = {
+ local = lib.mkIf cfg.local.enable {
+ inhibitsSleep = true;
initialize = true;
- backupPrepareCommand =
- # bash
- ''
- [ -d "${snapshotDir}" ] && ${lib.getExe' pkgs.btrfs-progs "btrfs"} subvolume delete "${snapshotDir}"
- # -r := Make the snapshot read-only
- ${lib.getExe' pkgs.btrfs-progs "btrfs"} subvolume snapshot -r /srv "${snapshotDir}";
- '';
- paths = [
- snapshotDir
- ];
- exclude = [
- "${homeDir}/soispha/.cache"
- ];
- extraBackupArgs = [
- "--verbose=2"
- ];
+ inherit paths exclude extraBackupArgs environmentFile;
+
+ passwordFile = config.age.secrets.resticLocalRepositoryPassword.path;
+
+ repository = "${cfg.local.backupMountPoint}/restic-backup-data/";
+
+ # Start on demand.
+ timerConfig = null;
+ };
- passwordFile = config.age.secrets.resticpass.path;
+ storagebox = lib.mkIf cfg.storagebox.enable {
+ inhibitsSleep = true;
+ initialize = true;
+
+ inherit paths exclude extraBackupArgs environmentFile;
+
+ passwordFile = config.age.secrets.resticStorageboxRepositoryPassword.path;
extraOptions = [
- "rclone.program='ssh -p 23 ${cfg.user}@${cfg.user}.your-storagebox.de -i ${config.age.secrets.resticssh.path} command_forced_on_remote'"
+ "rclone.program='ssh -p 23 ${cfg.storagebox.user}@${cfg.storagebox.user}.your-storagebox.de -i ${config.age.secrets.resticStorageboxSshKey.path} command_forced_on_remote'"
];
# This setting is normally passed to rclone, but we force
# the command on the remote.
- # As such, the value does not matter and must only be parseable by restic.
+ # As such, the value does not matter and must only be parse-able by restic.
repository = "rclone: ";
timerConfig = {
- Requires = "network-online.target";
OnActiveSec = "30m";
OnUnitInactiveSec = "2h";
Persistent = true;
};
};
+
+ # This is only for listing, pruning and such stuff.
+ storagebox-admin = lib.mkIf cfg.storagebox.enable {
+ inhibitsSleep = true;
+
+ # Don't create the repository if it doesn't exist yet.
+ initialize = false;
+
+ inherit paths exclude extraBackupArgs environmentFile;
+
+ passwordFile = config.age.secrets.resticStorageboxRepositoryPassword.path;
+ extraOptions = [
+ "rclone.program='ssh -p 23 ${cfg.storagebox.user}@${cfg.storagebox.user}.your-storagebox.de command_forced_on_remote'"
+ ];
+
+ # This setting is normally passed to rclone, but we force
+ # the command on the remote.
+ # As such, the value does not matter and must only be parse-able by restic.
+ repository = "rclone: ";
+
+ timerConfig = null;
+ };
};
};
}