diff --git a/hosts/shan/configuration.nix b/hosts/shan/configuration.nix index a9a6d9e..97753b0 100644 --- a/hosts/shan/configuration.nix +++ b/hosts/shan/configuration.nix @@ -68,24 +68,6 @@ isExitNode = true; tags = [ "server" ]; }; - - services.rsyncd = { - enable = true; - port = 9523; - settings = { - globalSection = { - address = "100.64.0.4"; - }; - sections = { - backup = { - path = "/var/backups/test"; - comment = "ftp export area"; - "write only" = true; - "read only" = false; - }; - }; - }; - }; }; services = { @@ -96,15 +78,8 @@ openFirewall = true; host = "0.0.0.0"; }; - }; - networking.firewall.allowedTCPPorts = [ 9523 ]; - - environment.systemPackages = map lib.lowPrio [ - pkgs.curl - ]; - users.users.root.openssh.authorizedKeys.keys = [ pubkeys.ny ]; system.stateVersion = "23.11"; diff --git a/options/fixes/rsyncd.nix b/options/fixes/rsyncd.nix index 75074cb..902f67d 100644 --- a/options/fixes/rsyncd.nix +++ b/options/fixes/rsyncd.nix @@ -5,13 +5,12 @@ ... }: let - cfg = config.modules.services.rsyncd; + cfg = config.modules.fixes.services.rsyncd; settingsFormat = pkgs.formats.iniWithGlobalSection { }; configFile = settingsFormat.generate "rsyncd.conf" cfg.settings; in { - options.modules.services.rsyncd = { - + options.modules.fixes.services.rsyncd = { enable = lib.mkEnableOption "the rsync daemon"; port = lib.mkOption { @@ -21,10 +20,7 @@ in }; settings = lib.mkOption { - type = lib.types.oneOf [ - settingsFormat.type - (pkgs.formats.ini { }).type # Retrocompatibility - ]; + type = settingsFormat.type; default = { }; example = { globalSection = { @@ -53,16 +49,6 @@ in Configuration for rsyncd. See {manpage}`rsyncd.conf(5)`. ''; - apply = - val: - if (lib.typeOf val == "ini") then - { - sections = lib.removeAttrs val [ "global" ]; - globalSection = lib.mkIf (lib.hasAttrs "global") val.global; - - } - else - val; }; socketActivated = lib.mkOption { @@ -74,9 +60,6 @@ in }; config = lib.mkIf cfg.enable { - - modules.services.rsyncd.settings.globalSection.port = toString cfg.port; - systemd = let serviceConfigSecurity = { diff --git a/options/services/rsync/default.nix b/options/services/rsync/default.nix index a9bef6a..2ba517b 100644 --- a/options/services/rsync/default.nix +++ b/options/services/rsync/default.nix @@ -1,231 +1,6 @@ { - config, - lib, - pkgs, - ... -}: -let - inherit (lib) - mkIf - mkEnableOption - mkOption - concatStringsSep - ; - inherit (lib.types) - listOf - nonEmptyStr - package - enum - str - port - ; - 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 = str; - default = ""; - }; - - 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 = cfg.flags ++ [ - (mkIf cfg.incremental.enable "--delete") - (mkIf (cfg.exclude != "") "--exclude=\"${cfg.exclude}\"") - (mkIf cfg.incremental.enable "--backup-dir=\"${cfg.incremental.finalIncrementPath}\"") - "--port=${toString cfg.port}" - ]; - 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/ - ''; - }; - # 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 under which Rsync runs."; - }; - - port = mkOption { - type = port; - default = 9523; - }; - }; - - 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 = { }; - }; - }; + imports = [ + ./rsync-backup.nix + ./rsync-daemon.nix + ]; } diff --git a/options/services/rsync/rsync-backup.nix b/options/services/rsync/rsync-backup.nix new file mode 100644 index 0000000..b82e457 --- /dev/null +++ b/options/services/rsync/rsync-backup.nix @@ -0,0 +1,232 @@ +{ + config, + lib, + pkgs, + options, + ... +}: +let + inherit (lib) + mkIf + mkEnableOption + mkOption + concatStringsSep + ; + inherit (lib.types) + listOf + nonEmptyStr + package + enum + str + port + ; + 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 = str; + default = ""; + }; + + 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 = cfg.flags ++ [ + (mkIf cfg.incremental.enable "--delete") + (mkIf (cfg.exclude != "") "--exclude=\"${cfg.exclude}\"") + (mkIf cfg.incremental.enable "--backup-dir=\"${cfg.incremental.finalIncrementPath}\"") + "--port=${toString cfg.port}" + ]; + 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/ + ''; + }; + # 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 under 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 = { }; + }; + }; +} diff --git a/options/services/rsync/rsync-daemon.nix b/options/services/rsync/rsync-daemon.nix new file mode 100644 index 0000000..c2be19f --- /dev/null +++ b/options/services/rsync/rsync-daemon.nix @@ -0,0 +1,101 @@ +{ config, lib, ... }: +let + inherit (lib) + mkIf + mkOption + mkEnableOption + listToAttrs + ; + inherit (lib.types) + enum + listOf + submodule + nonEmptyStr + str + bool + ; + inherit (lib.my) mkPortOption slugify; + + cfg = config.modules.services.rsync-daemon; +in +{ + options.modules.services.rsync-daemon = { + enable = mkEnableOption "rsync daemon"; + openFirewall = mkOption { + type = bool; + default = false; + description = "Whether to open the firewall"; + }; + port = mkPortOption 9523 "rsyncd"; + address = mkOption { + type = nonEmptyStr; + default = "0.0.0.0"; + description = "The address rsyncd should listen on"; + }; + location = mkOption { + type = nonEmptyStr; + default = "/var/lib/rsync-backups"; + }; + + modules = mkOption { + default = { }; + type = listOf (submodule { + options = { + mode = mkOption { + type = enum [ + "read" + "write" + "both" + ]; + default = "read"; + }; + name = mkOption { + type = nonEmptyStr; + default = ""; + description = "MANDATORY: The name of the module"; + }; + comment = mkOption { + type = str; + default = ""; + description = "A descriptive comment for the module."; + }; + }; + }); + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + + systemd.tmpfiles.settings."10-nginx-daemon" = listToAttrs ( + map (mod: { + name = "${cfg.location}/${slugify mod.name}"; + value.d = { + group = "nogroup"; + mode = "0777"; + user = "nobody"; + }; + }) cfg.modules + ); + + modules.fixes.services.rsyncd = { + enable = true; + settings = { + globalSection = { + inherit (cfg) port address; + }; + sections = listToAttrs ( + map (mod: { + inherit (mod) name; + value = { + inherit (mod) comment; + path = "${cfg.location}/${slugify mod.name}"; + "read only" = mod.mode == "read" || mod.mode == "both"; + "write only" = mod.mode == "write" || mod.mode == "both"; + }; + }) cfg.modules + ); + }; + }; + }; +} diff --git a/parts/lib/functions.nix b/parts/lib/functions.nix index b0b767a..13cd79b 100644 --- a/parts/lib/functions.nix +++ b/parts/lib/functions.nix @@ -5,7 +5,7 @@ ... }: let - inherit (lib) mkIf; + inherit (lib) mkOption; in { @@ -44,6 +44,18 @@ in } ); + mkPortOption = + port: name: + lib.mkOption { + type = lib.types.port; + default = port; + description = "The port ${name} should listen on"; + }; + + mkOption' = + type: default: description: + mkOption { inherit type default description; }; + validatePath = s: if (builtins.pathExists s) then (builtins.baseNameOf s) else throw "${s} does not exist";