nixos/rsync: init module

This commit is contained in:
Lukas Wurzinger
2025-05-26 21:09:01 +02:00
parent 4e6b410068
commit e33798277b
5 changed files with 271 additions and 0 deletions

View File

@@ -30,6 +30,8 @@
- [Overseerr](https://overseerr.dev), a request management and media discovery tool for the Plex ecosystem. Available as [services.overseerr](#opt-services.overseerr.enable).
- [services.rsync](options.html#opt-services.rsync) has been added to simplify periodic directory syncing.
- [gtklock](https://github.com/jovanlanik/gtklock), a GTK-based lockscreen for Wayland. Available as [programs.gtklock](#opt-programs.gtklock.enable).
- [Chrysalis](https://github.com/keyboardio/Chrysalis), a graphical configurator for Kaleidoscope-powered keyboards. Available as [programs.chrysalis](#opt-programs.chrysalis.enable).

View File

@@ -924,6 +924,7 @@
./services/misc/rkvm.nix
./services/misc/rmfakecloud.nix
./services/misc/rshim.nix
./services/misc/rsync.nix
./services/misc/safeeyes.nix
./services/misc/sdrplay.nix
./services/misc/servarr/lidarr.nix

View File

@@ -0,0 +1,203 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.rsync;
inherit (lib) types;
inherit (utils.systemdUtils.unitOptions) unitOption;
in
{
options.services.rsync = {
enable = lib.mkEnableOption "periodic directory syncing via rsync";
package = lib.mkPackageOption pkgs "rsync" { };
jobs = lib.mkOption {
description = ''
Synchronization jobs to run.
'';
default = { };
type = types.attrsOf (
types.submodule {
options = {
sources = lib.mkOption {
type = types.nonEmptyListOf types.str;
example = [
"/srv/src1/"
"/srv/src2/"
];
description = ''
Source directories.
'';
};
destination = lib.mkOption {
type = types.str;
example = "/srv/dst";
description = ''
Destination directory.
'';
};
settings = lib.mkOption {
type =
let
simples = [
types.bool
types.str
types.int
types.float
];
in
types.attrsOf (
types.oneOf (
simples
++ [
(types.listOf (types.oneOf simples))
]
)
);
default = { };
example = {
verbose = true;
archive = true;
delete = true;
mkpath = true;
};
description = ''
Settings that should be passed to rsync via long options.
See {manpage}`rsync(1)` for available options.
'';
};
user = lib.mkOption {
type = types.str;
default = "root";
description = ''
The name of an existing user account under which the rsync process should run.
'';
};
group = lib.mkOption {
type = types.str;
default = "root";
description = ''
The name of an existing user group under which the rsync process should run.
'';
};
timerConfig = lib.mkOption {
type = types.nullOr (types.attrsOf unitOption);
default = {
OnCalendar = "daily";
Persistent = true;
};
description = ''
When to run the job.
'';
};
inhibit = lib.mkOption {
default = [ ];
type = types.listOf (types.strMatching "^[^:]+$");
example = [
"sleep"
];
description = ''
Run the rsync process with an inhibition lock taken;
see {manpage}`systemd-inhibit(1)` for a list of possible operations.
'';
};
};
}
);
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = lib.all (job: job.sources != [ ]) (lib.attrValues cfg.jobs);
message = ''
At least one source directory must be provided to rsync.
'';
}
];
systemd = lib.mkMerge (
lib.mapAttrsToList (
jobName: job:
let
systemdName = "rsync-job-${jobName}";
description = "Directory syncing via rsync job ${jobName}";
in
{
timers.${systemdName} = {
wantedBy = [
"timers.target"
];
inherit description;
inherit (job) timerConfig;
};
services.${systemdName} = {
inherit description;
serviceConfig = {
Type = "oneshot";
ExecStart =
let
settingsToCommandLine = lib.cli.toCommandLineGNU {
isLong = _: true;
};
inhibitArgs = [
(lib.getExe' config.systemd.package "systemd-inhibit")
"--mode"
"block"
"--who"
description
"--what"
(lib.concatStringsSep ":" job.inhibit)
"--why"
"Scheduled rsync job ${jobName}"
"--"
];
args =
(lib.optionals (job.inhibit != [ ]) inhibitArgs)
++ [ (lib.getExe cfg.package) ]
++ (settingsToCommandLine job.settings)
++ [ "--" ]
++ job.sources
++ [ job.destination ];
in
utils.escapeSystemdExecArgs args;
User = job.user;
Group = job.group;
NoNewPrivileges = true;
PrivateDevices = true;
ProtectSystem = "full";
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
MemoryDenyWriteExecute = true;
LockPersonality = true;
};
};
}
) cfg.jobs
);
};
meta.maintainers = [
lib.maintainers.lukaswrz
];
}

View File

@@ -1322,6 +1322,7 @@ in
rss-bridge = handleTest ./web-apps/rss-bridge { };
rss2email = handleTest ./rss2email.nix { };
rstudio-server = runTest ./rstudio-server.nix;
rsync = runTest ./rsync.nix;
rsyncd = runTest ./rsyncd.nix;
rsyslogd = handleTest ./rsyslogd.nix { };
rtkit = runTest ./rtkit.nix;

64
nixos/tests/rsync.nix Normal file
View File

@@ -0,0 +1,64 @@
{
name = "rsync";
nodes.machine = {
users.users.test.isNormalUser = true;
services.rsync = {
enable = true;
jobs = {
root = {
sources = [ "/root/src/" ];
destination = "/root/dst";
settings = {
archive = true;
delete = true;
mkpath = true;
};
timerConfig = {
OnCalendar = "daily";
Persistent = false;
};
inhibit = [ "sleep" ];
};
user = {
sources = [ "/home/test/src/" ];
destination = "/home/test/dst";
settings = {
archive = true;
delete = true;
mkpath = true;
};
timerConfig = {
OnCalendar = "daily";
Persistent = false;
};
user = "test";
group = "users";
};
};
};
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
machine.succeed("mkdir --parents /root/src")
machine.succeed("echo test data > /root/src/file.txt")
machine.start_job("rsync-job-root.service")
machine.succeed("""[[ 'test data' == "$(< /root/dst/file.txt)" ]]""")
machine.succeed("mkdir --parents /home/test/src")
machine.succeed("echo test data > /home/test/src/file.txt")
machine.start_job("rsync-job-user.service")
machine.succeed("""[[ 'test data' == "$(< /home/test/dst/file.txt)" ]]""")
machine.wait_for_unit("timers.target")
machine.require_unit_state("rsync-job-root.timer", "active")
machine.require_unit_state("rsync-job-user.timer", "active")
machine.shutdown()
'';
}