inherit functions in modules

This commit is contained in:
ibizaman 2024-11-09 17:25:13 +01:00 committed by Pierre Penninckx
parent 455e0bd94c
commit 78072504ab
2 changed files with 86 additions and 78 deletions

View file

@ -5,43 +5,47 @@ let
shblib = pkgs.callPackage ../../lib {}; shblib = pkgs.callPackage ../../lib {};
instanceOptions = { inherit (lib) concatStringsSep filterAttrs flatten literalExpression optionals optionalString listToAttrs mapAttrsToList mkEnableOption mkOption mkMerge;
enable = lib.mkEnableOption "shb restic. A disabled instance will not backup data anymore but still provides the helper tool to introspect and rollback snapshots"; inherit (lib) generators hasPrefix mkIf nameValuePair optionalAttrs removePrefix;
inherit (lib.types) attrsOf enum int ints listOf oneOf nonEmptyListOf nonEmptyStr nullOr path str submodule;
passphraseFile = lib.mkOption { instanceOptions = {
enable = mkEnableOption "shb restic. A disabled instance will not backup data anymore but still provides the helper tool to introspect and rollback snapshots";
passphraseFile = mkOption {
description = "Encryption key for the backups."; description = "Encryption key for the backups.";
type = lib.types.path; type = path;
}; };
user = lib.mkOption { user = mkOption {
description = '' description = ''
Unix user doing the backups. Must be the user owning the files to be backed up. Unix user doing the backups. Must be the user owning the files to be backed up.
''; '';
type = lib.types.str; type = str;
}; };
sourceDirectories = lib.mkOption { sourceDirectories = mkOption {
description = "Source directories."; description = "Source directories.";
type = lib.types.nonEmptyListOf lib.types.str; type = nonEmptyListOf str;
}; };
excludePatterns = lib.mkOption { excludePatterns = mkOption {
description = "Exclude patterns."; description = "Exclude patterns.";
type = lib.types.listOf lib.types.str; type = listOf str;
default = []; default = [];
}; };
repositories = lib.mkOption { repositories = mkOption {
description = "Repositories to back this instance to."; description = "Repositories to back this instance to.";
type = lib.types.nonEmptyListOf (lib.types.submodule { type = nonEmptyListOf (submodule {
options = { options = {
path = lib.mkOption { path = mkOption {
type = lib.types.str; type = str;
description = "Repository location"; description = "Repository location";
}; };
secrets = lib.mkOption { secrets = mkOption {
type = lib.types.attrsOf shblib.secretFileType; type = attrsOf shblib.secretFileType;
default = {}; default = {};
description = '' description = ''
Secrets needed to access the repository where the backups will be stored. Secrets needed to access the repository where the backups will be stored.
@ -50,7 +54,7 @@ let
and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets. and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets.
''; '';
example = lib.literalExpression '' example = literalExpression ''
{ {
AWS_ACCESS_KEY_ID = <path/to/secret>; AWS_ACCESS_KEY_ID = <path/to/secret>;
AWS_SECRET_ACCESS_KEY = <path/to/secret>; AWS_SECRET_ACCESS_KEY = <path/to/secret>;
@ -58,8 +62,8 @@ let
''; '';
}; };
timerConfig = lib.mkOption { timerConfig = mkOption {
type = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption; type = attrsOf utils.systemdUtils.unitOptions.unitOption;
default = { default = {
OnCalendar = "daily"; OnCalendar = "daily";
Persistent = true; Persistent = true;
@ -75,9 +79,9 @@ let
}); });
}; };
retention = lib.mkOption { retention = mkOption {
description = "For how long to keep backup files."; description = "For how long to keep backup files.";
type = lib.types.attrsOf (lib.types.oneOf [ lib.types.int lib.types.nonEmptyStr ]); type = attrsOf (oneOf [ int nonEmptyStr ]);
default = { default = {
keep_within = "1d"; keep_within = "1d";
keep_hourly = 24; keep_hourly = 24;
@ -87,73 +91,73 @@ let
}; };
}; };
hooks = lib.mkOption { hooks = mkOption {
description = "Hooks to run before or after the backup."; description = "Hooks to run before or after the backup.";
default = {}; default = {};
type = lib.types.submodule { type = submodule {
options = { options = {
before_backup = lib.mkOption { before_backup = mkOption {
description = "Hooks to run before backup"; description = "Hooks to run before backup";
type = lib.types.listOf lib.types.str; type = listOf str;
default = []; default = [];
}; };
after_backup = lib.mkOption { after_backup = mkOption {
description = "Hooks to run after backup"; description = "Hooks to run after backup";
type = lib.types.listOf lib.types.str; type = listOf str;
default = []; default = [];
}; };
}; };
}; };
}; };
limitUploadKiBs = lib.mkOption { limitUploadKiBs = mkOption {
type = lib.types.nullOr lib.types.int; type = nullOr int;
description = "Limit upload bandwidth to the given KiB/s amount."; description = "Limit upload bandwidth to the given KiB/s amount.";
default = null; default = null;
example = 8000; example = 8000;
}; };
limitDownloadKiBs = lib.mkOption { limitDownloadKiBs = mkOption {
type = lib.types.nullOr lib.types.int; type = nullOr int;
description = "Limit download bandwidth to the given KiB/s amount."; description = "Limit download bandwidth to the given KiB/s amount.";
default = null; default = null;
example = 8000; example = 8000;
}; };
}; };
repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (lib.strings.removePrefix "/" name); repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (removePrefix "/" name);
backupName = name: repository: "${name}_${repoSlugName repository.path}"; backupName = name: repository: "${name}_${repoSlugName repository.path}";
fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}"; fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}";
in in
{ {
options.shb.restic = { options.shb.restic = {
instances = lib.mkOption { instances = mkOption {
description = "Each instance is a backup setting"; description = "Each instance is a backup setting";
default = {}; default = {};
type = lib.types.attrsOf (lib.types.submodule { type = attrsOf (submodule {
options = instanceOptions; options = instanceOptions;
}); });
}; };
# Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23 # Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
performance = lib.mkOption { performance = mkOption {
description = "Reduce performance impact of backup jobs."; description = "Reduce performance impact of backup jobs.";
default = {}; default = {};
type = lib.types.submodule { type = submodule {
options = { options = {
niceness = lib.mkOption { niceness = mkOption {
type = lib.types.ints.between (-20) 19; type = ints.between (-20) 19;
description = "nice priority adjustment, defaults to 15 for ~20% CPU time of normal-priority process"; description = "nice priority adjustment, defaults to 15 for ~20% CPU time of normal-priority process";
default = 15; default = 15;
}; };
ioSchedulingClass = lib.mkOption { ioSchedulingClass = mkOption {
type = lib.types.enum [ "idle" "best-effort" "realtime" ]; type = enum [ "idle" "best-effort" "realtime" ];
description = "ionice scheduling class, defaults to best-effort IO. Only used for `restic backup`, `restic forget` and `restic check` commands."; description = "ionice scheduling class, defaults to best-effort IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
default = "best-effort"; default = "best-effort";
}; };
ioPriority = lib.mkOption { ioPriority = mkOption {
type = lib.types.nullOr (lib.types.ints.between 0 7); type = nullOr (ints.between 0 7);
description = "ionice priority, defaults to 7 for lowest priority IO. Only used for `restic backup`, `restic forget` and `restic check` commands."; description = "ionice priority, defaults to 7 for lowest priority IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
default = 7; default = 7;
}; };
@ -162,22 +166,22 @@ in
}; };
}; };
config = lib.mkIf (cfg.instances != {}) ( config = mkIf (cfg.instances != {}) (
let let
enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances; enabledInstances = filterAttrs (k: i: i.enable) cfg.instances;
in lib.mkMerge [ in mkMerge [
{ {
environment.systemPackages = lib.optionals (enabledInstances != {}) [ pkgs.restic ]; environment.systemPackages = optionals (enabledInstances != {}) [ pkgs.restic ];
systemd.tmpfiles.rules = systemd.tmpfiles.rules =
let let
mkRepositorySettings = name: instance: repository: lib.optionals (lib.hasPrefix "/" repository.path) [ mkRepositorySettings = name: instance: repository: optionals (hasPrefix "/" repository.path) [
"d '${repository.path}' 0750 ${instance.user} root - -" "d '${repository.path}' 0750 ${instance.user} root - -"
]; ];
mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.repositories; mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.repositories;
in in
lib.flatten (lib.attrsets.mapAttrsToList mkSettings cfg.instances); flatten (mapAttrsToList mkSettings cfg.instances);
services.restic.backups = services.restic.backups =
let let
@ -195,21 +199,21 @@ in
inherit (repository) timerConfig; inherit (repository) timerConfig;
pruneOpts = lib.mapAttrsToList (name: value: pruneOpts = mapAttrsToList (name: value:
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}" "--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
) instance.retention; ) instance.retention;
backupPrepareCommand = lib.strings.concatStringsSep "\n" instance.hooks.before_backup; backupPrepareCommand = concatStringsSep "\n" instance.hooks.before_backup;
backupCleanupCommand = lib.strings.concatStringsSep "\n" instance.hooks.after_backup; backupCleanupCommand = concatStringsSep "\n" instance.hooks.after_backup;
} // lib.attrsets.optionalAttrs (builtins.length instance.excludePatterns > 0) { } // optionalAttrs (builtins.length instance.excludePatterns > 0) {
exclude = instance.excludePatterns; exclude = instance.excludePatterns;
extraBackupArgs = extraBackupArgs =
(lib.optionals (instance.limitUploadKiBs != null) [ (optionals (instance.limitUploadKiBs != null) [
"--limit-upload=${toString instance.limitUploadKiBs}" "--limit-upload=${toString instance.limitUploadKiBs}"
]) ])
++ (lib.optionals (instance.limitDownloadKiBs != null) [ ++ (optionals (instance.limitDownloadKiBs != null) [
"--limit-download=${toString instance.limitDownloadKiBs}" "--limit-download=${toString instance.limitDownloadKiBs}"
]); ]);
}; };
@ -217,7 +221,7 @@ in
mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.repositories; mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.repositories;
in in
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSettings enabledInstances)); mkMerge (flatten (mapAttrsToList mkSettings enabledInstances));
systemd.services = systemd.services =
let let
@ -226,7 +230,7 @@ in
serviceName = fullName name repository; serviceName = fullName name repository;
in in
{ {
${serviceName} = lib.mkMerge [ ${serviceName} = mkMerge [
{ {
serviceConfig = { serviceConfig = {
Nice = cfg.performance.niceness; Nice = cfg.performance.niceness;
@ -235,7 +239,7 @@ in
BindReadOnlyPaths = instance.sourceDirectories; BindReadOnlyPaths = instance.sourceDirectories;
}; };
} }
(lib.attrsets.optionalAttrs (repository.secrets != {}) (optionalAttrs (repository.secrets != {})
{ {
serviceConfig.EnvironmentFile = [ serviceConfig.EnvironmentFile = [
"/run/secrets_restic/${serviceName}" "/run/secrets_restic/${serviceName}"
@ -245,12 +249,12 @@ in
}) })
]; ];
"${serviceName}-pre" = lib.mkIf (repository.secrets != {}) "${serviceName}-pre" = mkIf (repository.secrets != {})
(let (let
script = shblib.genConfigOutOfBandSystemd { script = shblib.genConfigOutOfBandSystemd {
config = repository.secrets; config = repository.secrets;
configLocation = "/run/secrets_restic/${serviceName}"; configLocation = "/run/secrets_restic/${serviceName}";
generator = name: v: pkgs.writeText "template" (lib.generators.toINIWithGlobalSection {} { globalSection = v; }); generator = name: v: pkgs.writeText "template" (generators.toINIWithGlobalSection {} { globalSection = v; });
user = instance.user; user = instance.user;
}; };
in in
@ -262,24 +266,24 @@ in
}; };
mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.repositories; mkSettings = name: instance: builtins.map (mkRepositorySettings name instance) instance.repositories;
in in
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSettings enabledInstances)); mkMerge (flatten (mapAttrsToList mkSettings enabledInstances));
} }
{ {
system.activationScripts = let system.activationScripts = let
mkEnv = name: instance: repository: mkEnv = name: instance: repository:
lib.nameValuePair "${fullName name repository}_gen" nameValuePair "${fullName name repository}_gen"
(shblib.replaceSecrets { (shblib.replaceSecrets {
userConfig = repository.secrets // { userConfig = repository.secrets // {
RESTIC_PASSWORD_FILE = instance.passphraseFile; RESTIC_PASSWORD_FILE = instance.passphraseFile;
RESTIC_REPOSITORY = repository.path; RESTIC_REPOSITORY = repository.path;
}; };
resultPath = "/run/secrets_restic_env/${fullName name repository}"; resultPath = "/run/secrets_restic_env/${fullName name repository}";
generator = name: v: pkgs.writeText (fullName name repository) (lib.generators.toINIWithGlobalSection {} { globalSection = v; }); generator = name: v: pkgs.writeText (fullName name repository) (generators.toINIWithGlobalSection {} { globalSection = v; });
user = instance.user; user = instance.user;
}); });
mkSettings = name: instance: builtins.map (mkEnv name instance) instance.repositories; mkSettings = name: instance: builtins.map (mkEnv name instance) instance.repositories;
in in
lib.listToAttrs (lib.flatten (lib.attrsets.mapAttrsToList mkSettings cfg.instances)); listToAttrs (flatten (mapAttrsToList mkSettings cfg.instances));
environment.systemPackages = let environment.systemPackages = let
mkResticBinary = name: instance: repository: mkResticBinary = name: instance: repository:
@ -290,7 +294,7 @@ in
''; '';
mkSettings = name: instance: builtins.map (mkResticBinary name instance) instance.repositories; mkSettings = name: instance: builtins.map (mkResticBinary name instance) instance.repositories;
in in
lib.flatten (lib.attrsets.mapAttrsToList mkSettings cfg.instances); flatten (mapAttrsToList mkSettings cfg.instances);
} }
]); ]);
} }

View file

@ -1,38 +1,42 @@
{ lib, ... }: { lib, ... }:
lib.types.submodule { let
freeformType = lib.types.anything; inherit (lib) mkOption;
inherit (lib.types) anything listOf nonEmptyListOf nullOr submodule str;
in
submodule {
freeformType = anything;
options = { options = {
user = lib.mkOption { user = mkOption {
description = "Unix user doing the backups."; description = "Unix user doing the backups.";
type = lib.types.str; type = str;
}; };
sourceDirectories = lib.mkOption { sourceDirectories = mkOption {
description = "Directories to backup."; description = "Directories to backup.";
type = lib.types.nonEmptyListOf lib.types.str; type = nonEmptyListOf str;
}; };
excludePatterns = lib.mkOption { excludePatterns = mkOption {
description = "Patterns to exclude."; description = "Patterns to exclude.";
type = lib.types.listOf lib.types.str; type = listOf str;
default = []; default = [];
}; };
hooks = lib.mkOption { hooks = mkOption {
description = "Hooks to run around the backup."; description = "Hooks to run around the backup.";
default = {}; default = {};
type = lib.types.submodule { type = submodule {
options = { options = {
before_backup = lib.mkOption { before_backup = mkOption {
description = "Hooks to run before backup"; description = "Hooks to run before backup";
type = lib.types.listOf lib.types.str; type = listOf str;
default = []; default = [];
}; };
after_backup = lib.mkOption { after_backup = mkOption {
description = "Hooks to run after backup"; description = "Hooks to run after backup";
type = lib.types.listOf lib.types.str; type = listOf str;
default = []; default = [];
}; };
}; };