Commit a1aafb6a2fcb
Changed files (6)
home
common
desktop
lib
modules
rsync-replica
systems
aion
home/common/desktop/mails.nix
@@ -101,4 +101,5 @@
# across reboots and prevent re-indexing
home = "${config.xdg.dataHome}/mu";
};
+ home.packages = with pkgs; [ mblaze ];
}
lib/default.nix
@@ -73,6 +73,7 @@
self.nixosModules.wireguard-server
self.nixosModules.govanityurl
self.nixosModules.gosmee
+ self.nixosModules.rsync-replica
inputs.agenix.nixosModules.default
../systems/new.nix
];
@@ -116,6 +117,7 @@
self.nixosModules.wireguard-server
self.nixosModules.govanityurl
self.nixosModules.gosmee
+ self.nixosModules.rsync-replica
inputs.agenix.nixosModules.default
inputs.lanzaboote.nixosModules.lanzaboote
homeInput.nixosModules.home-manager
@@ -179,6 +181,7 @@
self.nixosModules.wireguard-server
self.nixosModules.govanityurl
self.nixosModules.gosmee
+ self.nixosModules.rsync-replica
inputs.agenix.nixosModules.default
inputs.lanzaboote.nixosModules.lanzaboote
homeInput.nixosModules.home-manager
modules/rsync-replica/default.nix
@@ -0,0 +1,208 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+
+with lib;
+
+let
+ cfg = config.services.rsync-replica;
+
+ # Convert schedule shortcuts to systemd OnCalendar format
+ scheduleToCalendar =
+ schedule:
+ if schedule == "hourly" then
+ "hourly"
+ else if schedule == "daily" then
+ "daily"
+ else if schedule == "weekly" then
+ "weekly"
+ else
+ schedule;
+
+ # Generate systemd service for a single job
+ mkReplicaService = name: jobCfg: {
+ description = "Rsync replica job: ${name}";
+ serviceConfig = {
+ Type = "oneshot";
+ User = jobCfg.user;
+ Group = jobCfg.group;
+ };
+ script =
+ let
+ # Build rsync command for each source path
+ syncCommands = map (
+ sourcePath:
+ let
+ # Extract the basename for destination (e.g., /neo/videos -> videos)
+ basename = baseNameOf sourcePath;
+ destPath = "${jobCfg.destination}/${basename}";
+
+ # Base rsync arguments
+ baseArgs = [
+ "-aAX" # archive mode with ACLs and xattrs
+ "--verbose"
+ "--human-readable"
+ "--progress"
+ ]
+ ++ optional jobCfg.delete "--delete"
+ ++ optional jobCfg.deleteExcluded "--delete-excluded"
+ ++ jobCfg.rsyncArgs;
+
+ # SSH command with custom args
+ sshCmd = "ssh ${concatStringsSep " " jobCfg.sshArgs}";
+
+ # Full rsync command
+ rsyncArgs = concatStringsSep " " (
+ baseArgs
+ ++ [
+ "-e '${sshCmd}'"
+ "${jobCfg.source.user}@${jobCfg.source.host}:${sourcePath}/"
+ "${destPath}/"
+ ]
+ );
+ in
+ ''
+ echo "Syncing ${sourcePath} from ${jobCfg.source.host} to ${destPath}"
+ mkdir -p "${destPath}"
+ ${pkgs.rsync}/bin/rsync ${rsyncArgs}
+ ''
+ ) jobCfg.source.paths;
+ in
+ concatStringsSep "\n" syncCommands;
+
+ path = with pkgs; [
+ openssh
+ rsync
+ ];
+ };
+
+ # Generate systemd timer for a single job
+ mkReplicaTimer = name: jobCfg: {
+ description = "Timer for rsync replica job: ${name}";
+ wantedBy = [ "timers.target" ];
+ timerConfig = {
+ OnCalendar = scheduleToCalendar jobCfg.schedule;
+ Persistent = true;
+ RandomizedDelaySec = jobCfg.randomizedDelay;
+ };
+ };
+
+in
+{
+ options.services.rsync-replica = {
+ enable = mkEnableOption "rsync-based replication service";
+
+ jobs = mkOption {
+ type = types.attrsOf (
+ types.submodule {
+ options = {
+ source = {
+ host = mkOption {
+ type = types.str;
+ description = "Remote hostname to sync from";
+ example = "rhea";
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "root";
+ description = "SSH user to connect as";
+ };
+
+ paths = mkOption {
+ type = types.listOf types.str;
+ description = "List of absolute paths on the source host to sync";
+ example = [
+ "/neo/videos"
+ "/neo/pictures"
+ ];
+ };
+ };
+
+ destination = mkOption {
+ type = types.str;
+ description = "Local destination directory (source basenames will be created here)";
+ example = "/neo";
+ };
+
+ schedule = mkOption {
+ type = types.str;
+ default = "daily";
+ description = ''
+ When to run the sync. Can be "hourly", "daily", "weekly", or a systemd OnCalendar format.
+ See systemd.time(7) for OnCalendar format details.
+ '';
+ example = "daily";
+ };
+
+ delete = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Delete files in destination that don't exist in source (true replica)";
+ };
+
+ deleteExcluded = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Also delete excluded files from destination";
+ };
+
+ rsyncArgs = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = "Additional arguments to pass to rsync";
+ example = [
+ "--exclude=*.tmp"
+ "--bwlimit=10000"
+ ];
+ };
+
+ sshArgs = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = "Additional arguments to pass to SSH";
+ example = [
+ "-p 2222"
+ "-i /root/.ssh/id_ed25519"
+ ];
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "root";
+ description = "User to run the rsync job as";
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "root";
+ description = "Group to run the rsync job as";
+ };
+
+ randomizedDelay = mkOption {
+ type = types.str;
+ default = "0";
+ description = "Randomized delay before starting the job (systemd RandomizedDelaySec)";
+ example = "1h";
+ };
+ };
+ }
+ );
+ default = { };
+ description = "Rsync replication jobs to run";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.services = mapAttrs' (
+ name: jobCfg: nameValuePair "rsync-replica-${name}" (mkReplicaService name jobCfg)
+ ) cfg.jobs;
+
+ systemd.timers = mapAttrs' (
+ name: jobCfg: nameValuePair "rsync-replica-${name}" (mkReplicaTimer name jobCfg)
+ ) cfg.jobs;
+ };
+}
modules/rsync-replica/README.md
@@ -0,0 +1,231 @@
+# rsync-replica Module
+
+A NixOS module for declarative rsync-based replication between hosts.
+
+## Features
+
+- Declarative configuration of rsync replication jobs
+- Systemd service and timer integration
+- Support for multiple sync jobs
+- Full archive mode with ACLs and extended attributes (-aAX)
+- Optional deletion of files (true replica/mirror mode)
+- Customizable rsync and SSH arguments
+- Flexible scheduling (hourly, daily, weekly, or custom systemd OnCalendar)
+
+## Usage
+
+### Basic Configuration
+
+```nix
+services.rsync-replica = {
+ enable = true;
+ jobs = {
+ backup-from-server = {
+ source = {
+ host = "source-hostname";
+ user = "root";
+ paths = [
+ "/path/to/source1"
+ "/path/to/source2"
+ ];
+ };
+ destination = "/local/backup";
+ schedule = "daily";
+ delete = true;
+ };
+ };
+};
+```
+
+### Multiple Jobs
+
+```nix
+services.rsync-replica = {
+ enable = true;
+ jobs = {
+ media-sync = {
+ source = {
+ host = "media-server";
+ paths = [ "/media/videos" "/media/music" ];
+ };
+ destination = "/backup/media";
+ schedule = "daily";
+ };
+
+ documents-sync = {
+ source = {
+ host = "file-server";
+ paths = [ "/documents" ];
+ };
+ destination = "/backup/documents";
+ schedule = "hourly";
+ rsyncArgs = [ "--exclude=*.tmp" "--bwlimit=5000" ];
+ };
+ };
+};
+```
+
+## Options
+
+### `services.rsync-replica.enable`
+- Type: `boolean`
+- Default: `false`
+- Description: Enable the rsync-replica service
+
+### `services.rsync-replica.jobs.<name>`
+- Type: `attribute set`
+- Description: Rsync replication jobs to run
+
+#### Job Options
+
+##### `source.host`
+- Type: `string`
+- Description: Remote hostname to sync from
+- Example: `"rhea"`
+
+##### `source.user`
+- Type: `string`
+- Default: `"root"`
+- Description: SSH user to connect as
+
+##### `source.paths`
+- Type: `list of strings`
+- Description: List of absolute paths on the source host to sync
+- Example: `[ "/neo/videos" "/neo/pictures" ]`
+
+##### `destination`
+- Type: `string`
+- Description: Local destination directory. Source basenames will be created here.
+- Example: `"/neo"` (will create `/neo/videos` and `/neo/pictures`)
+
+##### `schedule`
+- Type: `string`
+- Default: `"daily"`
+- Description: When to run the sync. Can be "hourly", "daily", "weekly", or a systemd OnCalendar format.
+- Example: `"daily"` or `"*-*-* 02:00:00"` (daily at 2 AM)
+
+##### `delete`
+- Type: `boolean`
+- Default: `true`
+- Description: Delete files in destination that don't exist in source (true replica/mirror mode)
+
+##### `deleteExcluded`
+- Type: `boolean`
+- Default: `false`
+- Description: Also delete excluded files from destination
+
+##### `rsyncArgs`
+- Type: `list of strings`
+- Default: `[ ]`
+- Description: Additional arguments to pass to rsync
+- Example: `[ "--exclude=*.tmp" "--bwlimit=10000" ]`
+
+##### `sshArgs`
+- Type: `list of strings`
+- Default: `[ ]`
+- Description: Additional arguments to pass to SSH
+- Example: `[ "-p 2222" "-i /root/.ssh/id_ed25519" ]`
+
+##### `user`
+- Type: `string`
+- Default: `"root"`
+- Description: User to run the rsync job as
+
+##### `group`
+- Type: `string`
+- Default: `"root"`
+- Description: Group to run the rsync job as
+
+##### `randomizedDelay`
+- Type: `string`
+- Default: `"0"`
+- Description: Randomized delay before starting the job (systemd RandomizedDelaySec)
+- Example: `"1h"` (randomize start within 1 hour)
+
+## How It Works
+
+For each job, the module:
+1. Creates a systemd service `rsync-replica-<job-name>`
+2. Creates a systemd timer `rsync-replica-<job-name>` to trigger the service
+3. For each path in `source.paths`, syncs `user@host:/path/` to `destination/basename/`
+
+For example, if you have:
+```nix
+source = {
+ host = "rhea";
+ paths = [ "/neo/videos" "/neo/pictures" ];
+};
+destination = "/neo";
+```
+
+This will sync:
+- `root@rhea:/neo/videos/` → `/neo/videos/`
+- `root@rhea:/neo/pictures/` → `/neo/pictures/`
+
+## Management Commands
+
+```bash
+# Check status of a sync job
+systemctl status rsync-replica-<job-name>
+
+# View timer schedule
+systemctl list-timers rsync-replica-*
+
+# Manually trigger a sync
+systemctl start rsync-replica-<job-name>
+
+# View logs
+journalctl -u rsync-replica-<job-name>
+```
+
+## Prerequisites
+
+- SSH access from the destination host to the source host
+- SSH keys set up for passwordless authentication
+- Sufficient disk space on destination
+- Network connectivity between hosts
+
+## Common Use Cases
+
+### Daily Backup with Exclusions
+```nix
+services.rsync-replica.jobs.daily-backup = {
+ source = {
+ host = "production-server";
+ paths = [ "/var/lib/important-data" ];
+ };
+ destination = "/backup";
+ schedule = "daily";
+ delete = true;
+ rsyncArgs = [
+ "--exclude=cache/"
+ "--exclude=*.log"
+ "--exclude=tmp/"
+ ];
+};
+```
+
+### Bandwidth-Limited Sync
+```nix
+services.rsync-replica.jobs.slow-sync = {
+ source = {
+ host = "remote-server";
+ paths = [ "/large/dataset" ];
+ };
+ destination = "/local/copy";
+ schedule = "weekly";
+ rsyncArgs = [ "--bwlimit=1000" ]; # Limit to 1 MB/s
+};
+```
+
+### Custom SSH Port
+```nix
+services.rsync-replica.jobs.custom-port = {
+ source = {
+ host = "server-with-custom-port";
+ paths = [ "/data" ];
+ };
+ destination = "/backup";
+ sshArgs = [ "-p 2222" ];
+};
+```
systems/aion/extra.nix
@@ -15,6 +15,36 @@
endpoint = "${globals.net.vpn.endpoint}";
endpointPublicKey = "${globals.machines.kerkouane.net.vpn.pubkey}";
};
+
+ rsync-replica = {
+ enable = true;
+ jobs = {
+ rhea-backup = {
+ source = {
+ host = "rhea.sbr.pm";
+ user = "vincent";
+ paths = [
+ "/neo/documents"
+ "/neo/music"
+ "/neo/pictures"
+ "/neo/videos"
+ ];
+ };
+ destination = "/neo";
+ schedule = "daily";
+ delete = true; # Mirror mode: delete files in destination that don't exist in source
+ user = "vincent";
+ group = "users";
+ rsyncArgs = [
+ "--exclude=.Trash-*"
+ "--exclude=lost+found"
+ ];
+ sshArgs = [
+ "-o StrictHostKeyChecking=accept-new"
+ ];
+ };
+ };
+ };
};
networking.useDHCP = lib.mkDefault true;
flake.nix
@@ -138,6 +138,7 @@
wireguard-server = ./modules/wireguard-server.nix;
govanityurl = ./modules/govanityurl.nix;
gosmee = ./modules/gosmee.nix;
+ rsync-replica = ./modules/rsync-replica;
};
# system-manager configurations