summary refs log tree commit diff
path: root/nixos/modules/services/backup/duplicity.nix
blob: a8d5642486235f451141f887e47ed3959bb87060 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
{ config, lib, pkgs, ...}:

with lib;

let
  cfg = config.services.duplicity;

  stateDirectory = "/var/lib/duplicity";

  localTarget = if hasPrefix "file://" cfg.targetUrl
    then removePrefix "file://" cfg.targetUrl else null;

in {
  options.services.duplicity = {
    enable = mkEnableOption "backups with duplicity";

    root = mkOption {
      type = types.path;
      default = "/";
      description = ''
        Root directory to backup.
      '';
    };

    include = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [ "/home" ];
      description = ''
        List of paths to include into the backups. See the FILE SELECTION
        section in <citerefentry><refentrytitle>duplicity</refentrytitle>
        <manvolnum>1</manvolnum></citerefentry> for details on the syntax.
      '';
    };

    exclude = mkOption {
      type = types.listOf types.str;
      default = [];
      description = ''
        List of paths to exclude from backups. See the FILE SELECTION section in
        <citerefentry><refentrytitle>duplicity</refentrytitle>
        <manvolnum>1</manvolnum></citerefentry> for details on the syntax.
      '';
    };

    targetUrl = mkOption {
      type = types.str;
      example = "s3://host:port/prefix";
      description = ''
        Target url to backup to. See the URL FORMAT section in
        <citerefentry><refentrytitle>duplicity</refentrytitle>
        <manvolnum>1</manvolnum></citerefentry> for supported urls.
      '';
    };

    secretFile = mkOption {
      type = types.nullOr types.path;
      default = null;
      description = ''
        Path of a file containing secrets (gpg passphrase, access key...) in
        the format of EnvironmentFile as described by
        <citerefentry><refentrytitle>systemd.exec</refentrytitle>
        <manvolnum>5</manvolnum></citerefentry>. For example:
        <programlisting>
        PASSPHRASE=<replaceable>...</replaceable>
        AWS_ACCESS_KEY_ID=<replaceable>...</replaceable>
        AWS_SECRET_ACCESS_KEY=<replaceable>...</replaceable>
        </programlisting>
      '';
    };

    frequency = mkOption {
      type = types.nullOr types.str;
      default = "daily";
      description = ''
        Run duplicity with the given frequency (see
        <citerefentry><refentrytitle>systemd.time</refentrytitle>
        <manvolnum>7</manvolnum></citerefentry> for the format).
        If null, do not run automatically.
      '';
    };

    extraFlags = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [ "--full-if-older-than" "1M" ];
      description = ''
        Extra command-line flags passed to duplicity. See
        <citerefentry><refentrytitle>duplicity</refentrytitle>
        <manvolnum>1</manvolnum></citerefentry>.
      '';
    };
  };

  config = mkIf cfg.enable {
    systemd = {
      services.duplicity = {
        description = "backup files with duplicity";

        environment.HOME = stateDirectory;

        serviceConfig = {
          ExecStart = ''
            ${pkgs.duplicity}/bin/duplicity ${escapeShellArgs (
              [
                cfg.root
                cfg.targetUrl
                "--archive-dir" stateDirectory
              ]
              ++ concatMap (p: [ "--include" p ]) cfg.include
              ++ concatMap (p: [ "--exclude" p ]) cfg.exclude
              ++ cfg.extraFlags)}
          '';
          PrivateTmp = true;
          ProtectSystem = "strict";
          ProtectHome = "read-only";
          StateDirectory = baseNameOf stateDirectory;
        } // optionalAttrs (localTarget != null) {
          ReadWritePaths = localTarget;
        } // optionalAttrs (cfg.secretFile != null) {
          EnvironmentFile = cfg.secretFile;
        };
      } // optionalAttrs (cfg.frequency != null) {
        startAt = cfg.frequency;
      };

      tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -";
    };

    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 != [];
      message = ''
        Duplicity will fail if you only specify included paths ("Because the
        default is to include all files, the expression is redundant. Exiting
        because this probably isn't what you meant.")
      '';
    };
  };
}