187 lines
5.4 KiB
Nix
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
|
|
'';
|
|
};
|
|
}
|