feat(rsync): allow a client/server to have multiple modules

This commit is contained in:
Nydragon 2024-10-13 19:55:35 +02:00
parent 9d105e3b84
commit 169ada30f6
Signed by: nydragon
SSH key fingerprint: SHA256:iQnIC12spf4QjWSbarmkD2No1cLMlu6TWoV7K6cYF5g
9 changed files with 344 additions and 262 deletions

View file

@ -1,4 +1,3 @@
{ pubkeys, ... }:
{
imports = [
./hardware-configuration.nix
@ -13,7 +12,7 @@
];
extraFlags = [ "--accept-dns=false" ]; # Want to disable that since *server* can't access the private dns... for now
};
services = {
server = {
rsync-daemon = {
enable = true;
port = 9523;
@ -26,6 +25,11 @@
comment = "backups for paperless";
mode = "write";
}
{
name = "immich-backup";
comment = "backups for immich";
mode = "write";
}
{
name = "brontes-backup";
comment = "brontes's backup space";

View file

@ -72,6 +72,8 @@
rsync-backup = {
enable = true;
modules = [
{
sources = [ "/var/lib/paperless" ];
target = {
location = "paperless-backup";
@ -79,6 +81,17 @@
host = "nihilus";
};
incremental.enable = true;
}
{
sources = [ "/var/lib/immich" ];
target = {
location = "immich-backup";
type = "rsyncd";
host = "nihilus";
};
incremental.enable = true;
}
];
};
};
};

View file

@ -2,5 +2,6 @@
imports = [
./paperless-ngx
./navidrome.nix
./rsync-daemon
];
}

View file

@ -16,10 +16,10 @@ let
;
inherit (lib.my) mkPortOption slugify;
cfg = config.modules.services.rsync-daemon;
cfg = config.modules.server.rsync-daemon;
in
{
options.modules.services.rsync-daemon = {
options.modules.server.rsync-daemon = {
enable = mkEnableOption "rsync daemon";
openFirewall = mkOption {
type = bool;

View file

@ -2,6 +2,6 @@
imports = [
./nysh.nix
./tailscale.nix
./rsync
./rsync-backup
];
}

View file

@ -0,0 +1,176 @@
{
config,
lib,
pkgs,
options,
...
}:
let
inherit (lib)
mkIf
mkEnableOption
mkOption
concatStringsSep
toList
listToAttrs
;
inherit (lib.types)
listOf
nonEmptyStr
package
str
port
bool
submoduleWith
;
inherit (lib.my) slugify;
cfg = config.modules.services.rsync-backup;
in
{
# Used to back up multiple source destinations to a target destination regularly,
# with possibility to enable incremental backups for versioning.
# TODO: systemd unit
# - [x] script taking configurable args
# - [x] sync from/to paths
# - [ ] support
# - [ ] nfs
# - [ ] sftp
# - [ ] ssh
# - [x] local
# - [x] rsyncd
# - [ ] retention limit for incremental backups
options.modules.services.rsync-backup = {
enable = mkEnableOption "rsync backups";
modules = mkOption {
default = [ ];
type = listOf (submoduleWith {
shorthandOnlyDefinesConfig = true;
modules = toList (import ./module.nix);
specialArgs = {
rootCfg = cfg;
};
});
};
exclude = mkOption {
type = listOf str;
default = [ ".git" ];
};
followGitignore = mkOption {
type = bool;
default = true;
};
package = mkOption {
type = package;
default = pkgs.rsync;
description = ''
The rsync package to use.
'';
};
interval = mkOption {
type = nonEmptyStr;
default = "daily";
};
unitName = mkOption {
type = nonEmptyStr;
default = "rsync-backup";
};
user = mkOption {
type = str;
default = "rsync-backup";
description = "User account under which Rsync runs.";
};
group = mkOption {
type = str;
default = "rsync-backup";
description = "Group under which Rsync runs.";
};
port = mkOption {
type = port;
default = options.modules.services.rsync-daemon.port.default;
};
};
config = mkIf cfg.enable {
systemd.timers = listToAttrs (
map (
mod:
let
name = "${cfg.unitName}-${slugify mod.target.location}";
in
{
inherit name;
value = {
enable = true;
wantedBy = [ "timers.target" ];
unitConfig = {
Description = "Triggers rsync-backups regularly.";
};
timerConfig = {
Unit = "${name}.service";
OnCalendar = mod.interval;
Persistent = true;
};
};
}
) cfg.modules
);
systemd.services = listToAttrs (
map (mod: {
name = "${cfg.unitName}-${slugify mod.target.location}";
value = {
script =
let
destination =
if mod.incremental.enable then "${mod.target.finalLocation}/backup" else mod.target.finalLocation;
sources = concatStringsSep " " (map (s: "\"${s}\"") mod.sources);
in
''
${cfg.package}/bin/rsync \
${mod.flagsFinal} \
${sources} \
"${destination}"
'';
requires = mkIf (mod.target.type == "rsyncd") [ "network.target" ];
unitConfig = {
Description = "Backs up files from a source location to a specified destination.";
};
serviceConfig = {
Type = "simple";
Restart = "on-failure";
RestartSec = 300;
PrivateDevices = true;
PrivateTmp = true;
ProtectSystem = "full";
ReadOnlyPaths = concatStringsSep " " mod.sources;
IPAddressAllow = "any";
};
};
}) cfg.modules
);
users.users = mkIf (cfg.user == "rsync-backup") {
rsync-backup = {
useDefaultShell = true;
group = cfg.group;
isSystemUser = true;
};
};
users.groups = mkIf (cfg.group == "rsync-backup") {
rsync-backup = { };
};
};
}

View file

@ -0,0 +1,138 @@
{
config,
lib,
specialArgs,
...
}:
let
inherit (lib)
mkOption
concatLists
concatStringsSep
mkEnableOption
;
inherit (lib.types)
listOf
nonEmptyStr
enum
str
bool
;
inherit (lib.lists) optionals;
inherit (specialArgs) rootCfg;
in
{
options = {
exclude = mkOption {
type = listOf nonEmptyStr;
default = rootCfg.exclude;
};
followGitignore = mkOption {
type = bool;
default = rootCfg.followGitignore;
};
sources = mkOption {
type = listOf nonEmptyStr;
default = [ ]; # TODO: validate if at least 1 element
description = ''
A list of absolute paths to the files or folders that should get synchronized.
It isn't necessary to wrap them in escaped double quotation marks.
'';
};
target = {
location = mkOption {
type = nonEmptyStr;
description = ''
The target to where the data should be backed up.
If it is a directory, it will be created automatically by rsync.
'';
};
finalLocation = mkOption {
type = nonEmptyStr;
default =
if config.target.type == "local" then
config.target.location
else if config.target.type == "rsyncd" then
"rsync://${config.target.host}/${config.target.location}"
else
throw "Unable to create location.";
};
type = mkOption {
type = enum [
"local"
"rsyncd"
];
default = "local";
description = ''
Guesses if the given target is local or ssh, very rudimentary therefore it might be necessary to specify manually (or rewrite the logic).
'';
};
host = mkOption {
type = str;
default = "";
};
};
interval = mkOption {
type = nonEmptyStr;
default = "daily";
};
flags = mkOption {
type = listOf str;
default = [
"--archive"
"--verbose"
"--mkpath" # create destination's missing path components
"--safe-links" # ignore symlinks that point outside the tree
"--itemize-changes" # output a change-summary for all updates
];
};
flagsFinal = mkOption {
type = listOf str;
default = concatLists [
config.flags
(optionals config.incremental.enable [
"--delete"
"--backup-dir=\"${config.incremental.finalIncrementPath}\""
])
(optionals config.followGitignore [ "--filter=':- .gitignore'" ])
[ "--port=${toString rootCfg.port}" ]
(map (ex: "--exclude='${ex}'") config.exclude)
];
apply = concatStringsSep " ";
};
incremental = {
enable = mkEnableOption "incremental backups";
format = mkOption {
type = nonEmptyStr;
default = "%Y-%m-%d-%-H-%M-%S-$RANDOM";
description = ''
The increment folder name. Refer to `man date` for specifics about the format.
Omit the leading +.
'';
};
finalIncrementPath = mkOption {
type = nonEmptyStr;
default = "../increment/$(date +${config.incremental.format})/";
description = ''
The directory in which the changes will be stored, the .. at the start is necessary because it is relative to
the target and we append `backup` to the target.
-- cfg.location
| - backup/
| - increment/<increment format>
'';
};
};
};
}

View file

@ -1,6 +0,0 @@
{
imports = [
./rsync-backup.nix
./rsync-daemon.nix
];
}

View file

@ -1,244 +0,0 @@
{
config,
lib,
pkgs,
options,
...
}:
let
inherit (lib)
mkIf
mkEnableOption
mkOption
concatStringsSep
concatLists
;
inherit (lib.lists) optionals;
inherit (lib.types)
listOf
nonEmptyStr
package
enum
str
port
bool
;
cfg = config.modules.services.rsync-backup;
in
{
# Used to back up multiple source destinations to a target destination regularly,
# with possibility to enable incremental backups for versioning.
# TODO: systemd unit
# - [x] script taking configurable args
# - [x] sync from/to paths
# - [ ] support
# - [ ] nfs
# - [ ] sftp
# - [ ] ssh
# - [x] local
# - [x] rsyncd
# - [ ] retention limit for incremental backups
options.modules.services.rsync-backup = {
enable = mkEnableOption "rsync backups";
sources = mkOption {
type = listOf nonEmptyStr;
default = [ ]; # TODO: validate if at least 1 element
description = ''
A list of absolute paths to the files or folders that should get synchronized.
It isn't necessary to wrap them in escaped double quotation marks.
'';
};
exclude = mkOption {
type = listOf str;
default = [ ".git" ];
};
followGitignore = mkOption {
type = bool;
default = true;
};
target = {
location = mkOption {
type = nonEmptyStr;
description = ''
The target to where the data should be backed up.
If it is a directory, it will be created automatically by rsync.
Append a : to the string if it is an ssh target and ensure that passwordless access is available.
'';
};
finalLocation = mkOption {
type = nonEmptyStr;
default =
if cfg.target.type == "local" then
cfg.target.location
else if cfg.target.type == "rsyncd" then
"rsync://${cfg.target.host}/${cfg.target.location}"
else
throw "Unable to create location.";
};
type = mkOption {
type = enum [
"local"
"rsyncd"
];
default = "local";
description = ''
Guesses if the given target is local or ssh, very rudimentary therefore it might be necessary to specify manually (or rewrite the logic).
'';
};
host = mkOption {
type = str;
default = "";
};
};
package = mkOption {
type = package;
default = pkgs.rsync;
description = ''
The rsync package to use.
'';
};
flags = mkOption {
type = listOf str;
default = [
"--archive"
"--verbose"
"--mkpath"
];
};
flagsFinal = mkOption {
type = listOf str;
default = concatLists [
cfg.flags
(optionals cfg.incremental.enable [
"--delete"
"--backup-dir='${cfg.incremental.finalIncrementPath}'"
])
(optionals cfg.followGitignore [ "--filter=':- .gitignore'" ])
[ "--port=${toString cfg.port}" ]
(map (ex: "--exclude='${ex}'") cfg.exclude)
];
apply = concatStringsSep " ";
};
interval = mkOption {
type = nonEmptyStr;
default = "daily";
};
incremental = {
enable = mkEnableOption "incremental backups";
format = mkOption {
type = nonEmptyStr;
default = "%Y-%m-%d-%-H-%M-%S-$RANDOM";
description = ''
The increment folder name. Refer to `man date` for specifics about the format.
Omit the leading +.
'';
};
finalIncrementPath = mkOption {
type = nonEmptyStr;
default = "../${cfg.target.location}/increment/$(date +${cfg.incremental.format})/";
description = ''
The directory in which the changes will be stored, the .. at the start is necessary because it is relative to
the target and we append `backup` to the target.
-- cfg.location
| - backup/
| - increment/<increment format>
'';
};
# TODO: add retention
};
unitName = mkOption {
type = nonEmptyStr;
default = "rsync-backup";
};
user = mkOption {
type = str;
default = "rsync-backup";
description = "User account under which Rsync runs.";
};
group = mkOption {
type = str;
default = "rsync-backup";
description = "Group under which Rsync runs.";
};
port = mkOption {
type = port;
default = options.modules.services.rsync-daemon.port.default;
};
};
config = mkIf cfg.enable {
systemd.timers.${cfg.unitName} = {
enable = true;
wantedBy = [ "timers.target" ];
unitConfig = {
Description = "Triggers rsync-backup regularly.";
};
timerConfig = {
Unit = "${cfg.unitName}.service";
OnCalendar = cfg.interval;
Persistent = true;
};
};
systemd.services.${cfg.unitName} = {
script =
let
destination =
if cfg.incremental.enable then "${cfg.target.finalLocation}/backup" else cfg.target.finalLocation;
sources = concatStringsSep " " (map (s: "\"${s}\"") cfg.sources);
in
''
${cfg.package}/bin/rsync \
${cfg.flagsFinal} \
${sources} \
"${destination}"
'';
requires = mkIf (cfg.target.type == "rsyncd") [ "network.target" ];
unitConfig = {
Description = "Backs up files from a source location to a specified destination.";
};
serviceConfig = {
Type = "simple";
Restart = "on-failure";
RestartSec = 300;
PrivateDevices = true;
PrivateTmp = true;
ProtectSystem = "full";
ReadOnlyPaths = concatStringsSep " " cfg.sources;
IPAddressAllow = "any";
};
};
users.users = mkIf (cfg.user == "rsync-backup") {
rsync-backup = {
useDefaultShell = true;
group = cfg.group;
isSystemUser = true;
};
};
users.groups = mkIf (cfg.group == "rsync-backup") {
rsync-backup = { };
};
};
}