Merge pull request #120622 from symphorien/duplicity-master

nixos/duplicity: enable to prevent backup from growing infinitely

Changed files
+73 -18
nixos
modules
services
backup
+73 -18
nixos/modules/services/backup/duplicity.nix
···
-
{ config, lib, pkgs, ...}:
+
{ config, lib, pkgs, ... }:
with lib;
-
let
cfg = config.services.duplicity;
stateDirectory = "/var/lib/duplicity";
-
localTarget = if hasPrefix "file://" cfg.targetUrl
+
localTarget =
+
if hasPrefix "file://" cfg.targetUrl
then removePrefix "file://" cfg.targetUrl else null;
-
in {
+
in
+
{
options.services.duplicity = {
enable = mkEnableOption "backups with duplicity";
···
include = mkOption {
type = types.listOf types.str;
-
default = [];
+
default = [ ];
example = [ "/home" ];
description = ''
List of paths to include into the backups. See the FILE SELECTION
···
exclude = mkOption {
type = types.listOf types.str;
-
default = [];
+
default = [ ];
description = ''
List of paths to exclude from backups. See the FILE SELECTION section in
<citerefentry><refentrytitle>duplicity</refentrytitle>
···
extraFlags = mkOption {
type = types.listOf types.str;
-
default = [];
-
example = [ "--full-if-older-than" "1M" ];
+
default = [ ];
+
example = [ "--backend-retry-delay" "100" ];
description = ''
Extra command-line flags passed to duplicity. See
<citerefentry><refentrytitle>duplicity</refentrytitle>
<manvolnum>1</manvolnum></citerefentry>.
'';
};
+
+
fullIfOlderThan = mkOption {
+
type = types.str;
+
default = "never";
+
example = "1M";
+
description = ''
+
If <literal>"never"</literal> (the default) always do incremental
+
backups (the first backup will be a full backup, of course). If
+
<literal>"always"</literal> always do full backups. Otherwise, this
+
must be a string representing a duration. Full backups will be made
+
when the latest full backup is older than this duration. If this is not
+
the case, an incremental backup is performed.
+
'';
+
};
+
+
cleanup = {
+
maxAge = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
example = "6M";
+
description = ''
+
If non-null, delete all backup sets older than the given time. Old backup sets
+
will not be deleted if backup sets newer than time depend on them.
+
'';
+
};
+
maxFull = mkOption {
+
type = types.nullOr types.int;
+
default = null;
+
example = 2;
+
description = ''
+
If non-null, delete all backups sets that are older than the count:th last full
+
backup (in other words, keep the last count full backups and
+
associated incremental sets).
+
'';
+
};
+
maxIncr = mkOption {
+
type = types.nullOr types.int;
+
default = null;
+
example = 1;
+
description = ''
+
If non-null, delete incremental sets of all backups sets that are
+
older than the count:th last full backup (in other words, keep only
+
old full backups and not their increments).
+
'';
+
};
+
};
};
config = mkIf cfg.enable {
···
environment.HOME = stateDirectory;
-
serviceConfig = {
-
ExecStart = ''
-
${pkgs.duplicity}/bin/duplicity ${escapeShellArgs (
-
[
-
cfg.root
-
cfg.targetUrl
-
"--archive-dir" stateDirectory
-
]
+
script =
+
let
+
target = escapeShellArg cfg.targetUrl;
+
extra = escapeShellArgs ([ "--archive-dir" stateDirectory ] ++ cfg.extraFlags);
+
dup = "${pkgs.duplicity}/bin/duplicity";
+
in
+
''
+
set -x
+
${dup} cleanup ${target} --force ${extra}
+
${lib.optionalString (cfg.cleanup.maxAge != null) "${dup} remove-older-than ${lib.escapeShellArg cfg.cleanup.maxAge} ${target} --force ${extra}"}
+
${lib.optionalString (cfg.cleanup.maxFull != null) "${dup} remove-all-but-n-full ${toString cfg.cleanup.maxFull} ${target} --force ${extra}"}
+
${lib.optionalString (cfg.cleanup.maxIncr != null) "${dup} remove-all-incr-but-n-full ${toString cfg.cleanup.maxIncr} ${target} --force ${extra}"}
+
exec ${dup} ${if cfg.fullIfOlderThan == "always" then "full" else "incr"} ${lib.escapeShellArgs (
+
[ cfg.root cfg.targetUrl ]
++ concatMap (p: [ "--include" p ]) cfg.include
++ concatMap (p: [ "--exclude" p ]) cfg.exclude
-
++ cfg.extraFlags)}
+
++ (lib.optionals (cfg.fullIfOlderThan != "never" && cfg.fullIfOlderThan != "always") [ "--full-if-older-than" cfg.fullIfOlderThan ])
+
)} ${extra}
'';
+
serviceConfig = {
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = "read-only";
···
assertions = singleton {
# Duplicity will fail if the last file selection option is an include. It
# is not always possible to detect but this simple case can be caught.
-
assertion = cfg.include != [] -> cfg.exclude != [] || cfg.extraFlags != [];
+
assertion = cfg.include != [ ] -> cfg.exclude != [ ] || cfg.extraFlags != [ ];
message = ''
Duplicity will fail if you only specify included paths ("Because the
default is to include all files, the expression is redundant. Exiting