homelab/modules/nixos/backup-manager.nix
plasmagoat bcbcc8b17b
Some checks failed
Test / tests (push) Has been cancelled
/ OpenTofu (push) Has been cancelled
homelab framework module init (everything is a mess)
2025-07-28 02:05:13 +02:00

187 lines
5.4 KiB
Nix

# modules/backup-manager.nix
{
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.homelab.backups;
globalCfg = config.homelab.global;
# Create systemd services for backup jobs
createBackupService = job: let
serviceName = "backup-${job.name}";
allExcludes = globalCfg.backups.globalExcludes ++ job.excludePatterns;
excludeArgs = map (pattern: "--exclude '${pattern}'") allExcludes;
backupScript =
if job.backend == "restic"
then ''
#!/bin/bash
set -euo pipefail
${optionalString (job.preHook != null) job.preHook}
# Restic backup
${pkgs.restic}/bin/restic backup \
${concatStringsSep " " (map (path: "'${path}'") job.paths)} \
${concatStringsSep " " excludeArgs} \
--tag "host:${globalCfg.hostname}" \
--tag "job:${job.name}" \
--tag "env:${globalCfg.environment}"
# Apply retention policy
${pkgs.restic}/bin/restic forget \
--keep-daily ${job.retention.daily} \
--keep-weekly ${job.retention.weekly} \
--keep-monthly ${job.retention.monthly} \
--keep-yearly ${job.retention.yearly} \
--prune
${optionalString (job.postHook != null) job.postHook}
''
else if job.backend == "borg"
then ''
#!/bin/bash
set -euo pipefail
${optionalString (job.preHook != null) job.preHook}
# Borg backup
${pkgs.borgbackup}/bin/borg create \
--stats --progress \
${concatStringsSep " " excludeArgs} \
"::${globalCfg.hostname}-${job.name}-{now}" \
${concatStringsSep " " (map (path: "'${path}'") job.paths)}
# Apply retention policy
${pkgs.borgbackup}/bin/borg prune \
--keep-daily ${job.retention.daily} \
--keep-weekly ${job.retention.weekly} \
--keep-monthly ${job.retention.monthly} \
--keep-yearly ${job.retention.yearly}
${optionalString (job.postHook != null) job.postHook}
''
else throw "Unsupported backup backend: ${job.backend}";
in {
${serviceName} = {
description = "Backup job: ${job.name}";
after = ["network-online.target"];
wants = ["network-online.target"];
serviceConfig = {
Type = "oneshot";
User = "backup";
Group = "backup";
ExecStart = pkgs.writeScript "backup-${job.name}" backupScript;
EnvironmentFile = "/etc/backup/environment";
};
};
};
# Create systemd timers for backup jobs
createBackupTimer = job: let
serviceName = "backup-${job.name}";
timerName = "${serviceName}.timer";
in {
${timerName} = {
description = "Timer for backup job: ${job.name}";
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar =
if job.schedule == "daily"
then "daily"
else if job.schedule == "weekly"
then "weekly"
else if job.schedule == "hourly"
then "hourly"
else job.schedule; # Assume it's a cron expression
Persistent = true;
RandomizedDelaySec = "15min";
};
};
};
in {
options.homelab.backups = {
enable = mkEnableOption "Backup management";
restic = {
repository = mkOption {
type = types.str;
description = "Restic repository URL";
};
passwordFile = mkOption {
type = types.str;
default = "/etc/backup/restic-password";
description = "Path to file containing restic password";
};
};
borg = {
repository = mkOption {
type = types.str;
description = "Borg repository path";
};
sshKey = mkOption {
type = types.str;
default = "/etc/backup/borg-ssh-key";
description = "Path to SSH key for borg repository";
};
};
};
config = mkIf (cfg.enable && globalCfg.enable && (length globalCfg.backups.jobs) > 0) {
# Create backup user
users.users.backup = {
isSystemUser = true;
group = "backup";
home = "/var/lib/backup";
createHome = true;
};
users.groups.backup = {};
# Install backup tools
environment.systemPackages = with pkgs; [
restic
borgbackup
rclone
(pkgs.writeScriptBin "backup-status" ''
#!/bin/bash
echo "=== Backup Status ==="
echo
${concatStringsSep "\n" (map (job: ''
echo "Job: ${job.name}"
systemctl is-active backup-${job.name}.timer || echo "Timer inactive"
systemctl status backup-${job.name}.timer --no-pager -l | grep -E "(Active|Trigger)" || true
echo
'')
globalCfg.backups.jobs)}
'')
];
# Create systemd services and timers
systemd.services = lib.foldl' (acc: job: acc // (createBackupService job)) {} globalCfg.backups.jobs;
systemd.timers = lib.foldl' (acc: job: acc // (createBackupTimer job)) {} globalCfg.backups.jobs;
# Environment file template
environment.etc."backup/environment.example".text = ''
# Restic configuration
RESTIC_REPOSITORY=${cfg.restic.repository}
RESTIC_PASSWORD_FILE=${cfg.restic.passwordFile}
# AWS S3 credentials (if using S3 backend)
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
# Borg configuration
BORG_REPO=${cfg.borg.repository}
BORG_RSH="ssh -i ${cfg.borg.sshKey}"
# Notification settings
NOTIFICATION_URL=your-webhook-url
'';
};
}