# 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 ''; }; }