|
| 1 | +/* |
| 2 | + TODO: |
| 3 | +
|
| 4 | + - handle backup overlap |
| 5 | +*/ |
| 6 | + |
1 | 7 | { ... }: { |
2 | 8 | flake.customModules.rustic = |
3 | 9 | { |
|
14 | 20 | filterAttrs |
15 | 21 | mapAttrs' |
16 | 22 | nameValuePair |
| 23 | + mapAttrsToList |
| 24 | + optionalString |
17 | 25 | types |
18 | 26 | ; |
19 | 27 |
|
20 | 28 | format = pkgs.formats.toml { }; |
21 | 29 |
|
22 | 30 | cfg = config.services.rustic; |
23 | 31 |
|
24 | | - # Merge profile options into a TOML attrset. |
25 | | - # `snapshot` key from settings → injected into `backup.snapshots` (array of tables). |
26 | | - # `label` and `sources` come from profile options, not from settings. |
27 | | - genProfileConfig = |
28 | | - name: p: |
29 | | - let |
30 | | - label = if p.label == null then name else p.label; |
31 | | - baseCfg = builtins.removeAttrs p.settings [ "snapshot" ]; |
32 | | - snapEntry = { |
33 | | - inherit label; |
34 | | - sources = p.sources; |
35 | | - } |
36 | | - // (builtins.removeAttrs (p.settings.snapshot or { }) [ |
37 | | - "label" |
38 | | - "sources" |
39 | | - ]); |
40 | | - in |
41 | | - baseCfg |
42 | | - // { |
43 | | - backup = (baseCfg.backup or { }) // { |
44 | | - snapshots = [ snapEntry ]; |
45 | | - }; |
46 | | - }; |
| 32 | + # Resolve timer config: null → global default, else → override. |
| 33 | + effectiveTimerConfig = p: if p.timerConfig == null then cfg.timerConfig else p.timerConfig; |
| 34 | + |
| 35 | + enabled = filterAttrs (_: p: p.enable) cfg.backups; |
47 | 36 |
|
48 | | - # Resolve timer config: null → global default, {} → disabled, else → override. |
49 | | - effectiveTimerConfig = |
50 | | - p: |
51 | | - if p.timerConfig == null then |
52 | | - cfg.timerConfig |
53 | | - else if p.timerConfig == { } then |
54 | | - null |
55 | | - else |
56 | | - p.timerConfig; |
57 | | - |
58 | | - rusticBin = "${cfg.package}/bin/rustic"; |
| 37 | + mkWrapper = |
| 38 | + name: p: |
| 39 | + pkgs.writeShellScriptBin "rustic-${name}" '' |
| 40 | + set -a |
| 41 | + ${optionalString (p.environmentFile != null) ". ${p.environmentFile}"} |
| 42 | + set +a |
| 43 | + export RUSTIC_CACHE_DIR=/var/cache/rustic |
| 44 | + exec ${cfg.package}/bin/rustic -P ${name} "$@" |
| 45 | + ''; |
59 | 46 | in |
60 | 47 | { |
61 | 48 | options.services.rustic = { |
|
69 | 56 | }; |
70 | 57 |
|
71 | 58 | prometheus = { |
72 | | - enable = mkEnableOption "prometheus metrics push to VictoriaMetrics"; |
| 59 | + enable = mkEnableOption "rustic Prometheus metrics push"; |
73 | 60 | address = mkOption { |
74 | 61 | type = types.str; |
75 | | - default = "http://127.0.0.1:8428/api/v1/import/prometheus"; |
76 | | - description = "Prometheus remote write URL (VictoriaMetrics endpoint)."; |
| 62 | + description = "Prometheus remote write URL"; |
77 | 63 | }; |
78 | 64 | user = mkOption { |
79 | 65 | type = types.nullOr types.str; |
80 | 66 | default = null; |
81 | | - description = "Basic auth user for Prometheus Pushgateway (env RUSTIC_PROMETHEUS_USER)."; |
82 | | - }; |
83 | | - passwordFile = mkOption { |
84 | | - type = types.nullOr types.path; |
85 | | - default = null; |
86 | | - description = "File containing basic auth password for Prometheus Pushgateway (env RUSTIC_PROMETHEUS_PASS)."; |
| 67 | + description = "Basic auth user (env RUSTIC_PROMETHEUS_USER)."; |
87 | 68 | }; |
88 | 69 | }; |
89 | 70 |
|
|
93 | 74 | OnCalendar = "daily"; |
94 | 75 | Persistent = true; |
95 | 76 | }; |
96 | | - example = { |
97 | | - OnCalendar = "0/4:00:00"; |
98 | | - Persistent = true; |
99 | | - }; |
100 | 77 | description = '' |
101 | | - Default systemd timer config. Used by profiles without their own timerConfig. |
102 | | - Set profile's timerConfig to {} to disable timer (manual only). |
| 78 | + Default systemd timer config, e.g. { OnCalendar = "0/4:00:00"; Persistent = true; }. |
| 79 | + Used by profiles without their own timerConfig. |
103 | 80 | ''; |
104 | 81 | }; |
105 | 82 |
|
|
110 | 87 | options = { |
111 | 88 | enable = mkEnableOption "this backup profile"; |
112 | 89 |
|
113 | | - label = mkOption { |
114 | | - type = types.nullOr types.str; |
115 | | - default = null; |
116 | | - example = "services"; |
117 | | - description = "Snapshot label. Falls back to profile attribute name."; |
118 | | - }; |
119 | | - |
120 | | - sources = mkOption { |
121 | | - type = types.listOf types.path; |
122 | | - example = [ "/var/lib/9router" ]; |
123 | | - description = "Paths to back up."; |
124 | | - }; |
125 | | - |
126 | 90 | environmentFile = mkOption { |
127 | 91 | type = types.nullOr types.path; |
128 | 92 | default = null; |
129 | | - description = '' |
130 | | - Path to environment file with RUSTIC_REPOSITORY, RUSTIC_PASSWORD, |
131 | | - RUSTIC_REPOSITORY_OPTIONS_*. Usually a sops-decrypted dotenv file. |
132 | | - ''; |
| 93 | + description = "Path to environment file."; |
133 | 94 | }; |
134 | 95 |
|
135 | 96 | timerConfig = mkOption { |
|
145 | 106 | ''; |
146 | 107 | }; |
147 | 108 |
|
148 | | - # Free-form rustic TOML — each top-level key becomes a [section]. |
149 | | - # Use `snapshot = { ... }` for [[snapshot]] entries; sources auto-injected. |
150 | 109 | settings = mkOption { |
151 | 110 | type = types.attrsOf types.anything; |
152 | 111 | default = { }; |
153 | 112 | example = { |
154 | 113 | global.check-index = true; |
155 | 114 | backup.skip-if-unchanged = true; |
156 | 115 | backup."exclude-if-present" = [ ".nobackup" ]; |
| 116 | + backup.snapshots = [ |
| 117 | + { |
| 118 | + label = "services"; |
| 119 | + sources = [ "/var/lib/9router" ]; |
| 120 | + } |
| 121 | + ]; |
157 | 122 | forget."keep-daily" = 14; |
158 | 123 | forget."keep-weekly" = 8; |
159 | 124 | forget."keep-monthly" = 24; |
160 | | - snapshot = { |
161 | | - label = "services"; |
162 | | - }; |
163 | 125 | }; |
164 | 126 | description = '' |
165 | | - Full rustic TOML config. Each top-level attr = TOML section. |
166 | | - Use `snapshot` for [[snapshot]] entries (sources auto-injected). |
| 127 | + Full rustic TOML config, written verbatim to /etc/rustic/<name>.toml. |
167 | 128 | ''; |
168 | 129 | }; |
169 | 130 | }; |
170 | 131 | } |
171 | 132 | ) |
172 | 133 | ); |
173 | 134 | default = { }; |
174 | | - description = "Attribute set of backup profiles. Key = profile name."; |
| 135 | + description = "Attribute set of backup profiles. Key is profile name."; |
175 | 136 | }; |
176 | 137 | }; |
177 | 138 |
|
178 | 139 | config = mkIf cfg.enable { |
179 | | - environment.systemPackages = [ cfg.package ]; |
| 140 | + environment.systemPackages = [ cfg.package ] ++ mapAttrsToList mkWrapper enabled; |
180 | 141 |
|
181 | 142 | environment.etc = mapAttrs' ( |
182 | 143 | name: p: |
183 | 144 | nameValuePair "rustic/${name}.toml" { |
184 | | - source = format.generate "rustic-${name}.toml" (genProfileConfig name p); |
| 145 | + source = format.generate "rustic-${name}.toml" p.settings; |
185 | 146 | mode = "0440"; |
186 | 147 | } |
187 | | - ) (filterAttrs (_: p: p.enable) cfg.backups); |
| 148 | + ) enabled; |
188 | 149 |
|
189 | 150 | systemd.services = mapAttrs' ( |
190 | 151 | name: p: |
|
196 | 157 |
|
197 | 158 | serviceConfig = { |
198 | 159 | Type = "oneshot"; |
199 | | - ExecStart = "${rusticBin} backup -P ${name}"; |
| 160 | + ExecStart = "${cfg.package}/bin/rustic backup -P ${name}"; |
200 | 161 | Environment = [ |
201 | 162 | "RUSTIC_CONFIG_DIR=/etc/rustic" |
202 | 163 | "RUSTIC_CACHE_DIR=/var/cache/rustic" |
|
205 | 166 | ++ lib.optional ( |
206 | 167 | cfg.prometheus.enable && cfg.prometheus.user != null |
207 | 168 | ) "RUSTIC_PROMETHEUS_USER=${cfg.prometheus.user}"; |
208 | | - EnvironmentFile = |
209 | | - lib.optionals (cfg.prometheus.enable && cfg.prometheus.passwordFile != null) [ |
210 | | - cfg.prometheus.passwordFile |
211 | | - ] |
212 | | - ++ lib.optional (p.environmentFile != null) p.environmentFile; |
213 | | - ReadOnlyPaths = p.sources; |
| 169 | + EnvironmentFile = lib.optional (p.environmentFile != null) p.environmentFile; |
214 | 170 | ReadWritePaths = [ "/var/cache/rustic" ]; |
215 | 171 | CacheDirectory = "rustic"; |
216 | 172 | NoNewPrivileges = true; |
|
0 commit comments