diff options
-rw-r--r-- | hosts/surtr/borg.nix | 50 | ||||
-rw-r--r-- | hosts/surtr/default.nix | 2 | ||||
-rw-r--r-- | hosts/surtr/matrix/default.nix | 4 | ||||
-rw-r--r-- | modules/borgsnap/borgsnap/borgsnap/__main__.py | 4 | ||||
-rw-r--r-- | modules/coturn.nix | 369 | ||||
-rw-r--r-- | modules/zfssnap/default.nix | 34 |
6 files changed, 444 insertions, 19 deletions
diff --git a/hosts/surtr/borg.nix b/hosts/surtr/borg.nix new file mode 100644 index 00000000..b9fe53d7 --- /dev/null +++ b/hosts/surtr/borg.nix | |||
@@ -0,0 +1,50 @@ | |||
1 | { lib, config, ... }: | ||
2 | |||
3 | with lib; | ||
4 | |||
5 | { | ||
6 | config = { | ||
7 | services.borgsnap = { | ||
8 | enable = true; | ||
9 | target = "borg.vidhar:."; | ||
10 | |||
11 | extraConfig = mkForce { | ||
12 | daily = "31"; | ||
13 | monthly = "-1"; | ||
14 | }; | ||
15 | |||
16 | sshConfig = '' | ||
17 | Include /etc/ssh/ssh_config | ||
18 | |||
19 | ControlMaster auto | ||
20 | ControlPath /var/lib/borg/.borgssh-master-%r@%n:%p | ||
21 | ControlPersist yes | ||
22 | |||
23 | Host borg.vidhar | ||
24 | HostName vidhar.yggdrasil.li | ||
25 | User borg | ||
26 | IdentityFile ${config.sops.secrets."append.borg.vidhar".path} | ||
27 | IdentitiesOnly yes | ||
28 | |||
29 | BatchMode yes | ||
30 | ServerAliveInterval 10 | ||
31 | ServerAliveCountMax 30 | ||
32 | ''; | ||
33 | }; | ||
34 | |||
35 | sops.secrets."append.borg.vidhar" = { | ||
36 | format = "binary"; | ||
37 | sopsFile = ../vidhar/borg/jotnar/surtr; | ||
38 | owner = "borg"; | ||
39 | group = "borg"; | ||
40 | mode = "0400"; | ||
41 | }; | ||
42 | |||
43 | users.users.borg = { | ||
44 | useDefaultShell = true; | ||
45 | isSystemUser = true; | ||
46 | group = "borg"; | ||
47 | }; | ||
48 | users.groups.borg = {}; | ||
49 | }; | ||
50 | } | ||
diff --git a/hosts/surtr/default.nix b/hosts/surtr/default.nix index f616d749..cebb2b6c 100644 --- a/hosts/surtr/default.nix +++ b/hosts/surtr/default.nix | |||
@@ -2,7 +2,7 @@ | |||
2 | { | 2 | { |
3 | imports = with flake.nixosModules.systemProfiles; [ | 3 | imports = with flake.nixosModules.systemProfiles; [ |
4 | tmpfs-root qemu-guest openssh rebuild-machines zfs | 4 | tmpfs-root qemu-guest openssh rebuild-machines zfs |
5 | ./zfs.nix ./dns ./tls ./http ./bifrost ./matrix ./postgresql.nix ./prometheus ./email ./vpn | 5 | ./zfs.nix ./dns ./tls ./http ./bifrost ./matrix ./postgresql.nix ./prometheus ./email ./vpn ./borg.nix |
6 | ]; | 6 | ]; |
7 | 7 | ||
8 | config = { | 8 | config = { |
diff --git a/hosts/surtr/matrix/default.nix b/hosts/surtr/matrix/default.nix index 46c2f338..f5a411ac 100644 --- a/hosts/surtr/matrix/default.nix +++ b/hosts/surtr/matrix/default.nix | |||
@@ -228,10 +228,6 @@ with lib; | |||
228 | "turn.synapse.li" = { | 228 | "turn.synapse.li" = { |
229 | zone = "synapse.li"; | 229 | zone = "synapse.li"; |
230 | certCfg = { | 230 | certCfg = { |
231 | server = "https://acme.zerossl.com/v2/DV90"; | ||
232 | extraLegoFlags = [ | ||
233 | "--cert.timeout" "300" | ||
234 | ]; | ||
235 | postRun = '' | 231 | postRun = '' |
236 | ${pkgs.systemd}/bin/systemctl try-restart coturn.service | 232 | ${pkgs.systemd}/bin/systemctl try-restart coturn.service |
237 | ''; | 233 | ''; |
diff --git a/modules/borgsnap/borgsnap/borgsnap/__main__.py b/modules/borgsnap/borgsnap/borgsnap/__main__.py index 91144780..ad46a7bf 100644 --- a/modules/borgsnap/borgsnap/borgsnap/__main__.py +++ b/modules/borgsnap/borgsnap/borgsnap/__main__.py | |||
@@ -246,7 +246,9 @@ def create(*, snapshot, target, archive_prefix, dry_run): | |||
246 | env['BORG_FILES_CACHE_SUFFIX'] = basename | 246 | env['BORG_FILES_CACHE_SUFFIX'] = basename |
247 | archive_name = _archive_name(snapshot, target, archive_prefix) | 247 | archive_name = _archive_name(snapshot, target, archive_prefix) |
248 | target_host, _, target_path = target.rpartition(':') | 248 | target_host, _, target_path = target.rpartition(':') |
249 | *parents_init, _ = list(Path(target_path).parents) | 249 | parents_init = list() |
250 | if Path(target_path).parents: | ||
251 | *parents_init, _ = list(Path(target_path).parents) | ||
250 | backup_patterns = [*(map(lambda p: Path('.backup') / f'{target_host}:{p}', [Path(target_path), *parents_init])), Path('.backup') / target_host, Path('.backup')] | 252 | backup_patterns = [*(map(lambda p: Path('.backup') / f'{target_host}:{p}', [Path(target_path), *parents_init])), Path('.backup') / target_host, Path('.backup')] |
251 | for pattern_file in backup_patterns: | 253 | for pattern_file in backup_patterns: |
252 | if (dir / pattern_file).is_file(): | 254 | if (dir / pattern_file).is_file(): |
diff --git a/modules/coturn.nix b/modules/coturn.nix new file mode 100644 index 00000000..faa4b5a2 --- /dev/null +++ b/modules/coturn.nix | |||
@@ -0,0 +1,369 @@ | |||
1 | { config, lib, pkgs, ... }: | ||
2 | with lib; | ||
3 | let | ||
4 | cfg = config.services.coturn; | ||
5 | pidfile = "/run/turnserver/turnserver.pid"; | ||
6 | configFile = pkgs.writeText "turnserver.conf" '' | ||
7 | listening-port=${toString cfg.listening-port} | ||
8 | tls-listening-port=${toString cfg.tls-listening-port} | ||
9 | alt-listening-port=${toString cfg.alt-listening-port} | ||
10 | alt-tls-listening-port=${toString cfg.alt-tls-listening-port} | ||
11 | ${concatStringsSep "\n" (map (x: "listening-ip=${x}") cfg.listening-ips)} | ||
12 | ${concatStringsSep "\n" (map (x: "relay-ip=${x}") cfg.relay-ips)} | ||
13 | min-port=${toString cfg.min-port} | ||
14 | max-port=${toString cfg.max-port} | ||
15 | ${lib.optionalString cfg.lt-cred-mech "lt-cred-mech"} | ||
16 | ${lib.optionalString cfg.no-auth "no-auth"} | ||
17 | ${lib.optionalString cfg.use-auth-secret "use-auth-secret"} | ||
18 | ${lib.optionalString (cfg.static-auth-secret != null) ("static-auth-secret=${cfg.static-auth-secret}")} | ||
19 | ${lib.optionalString (cfg.static-auth-secret-file != null) ("static-auth-secret=#static-auth-secret#")} | ||
20 | realm=${cfg.realm} | ||
21 | ${lib.optionalString cfg.no-udp "no-udp"} | ||
22 | ${lib.optionalString cfg.no-tcp "no-tcp"} | ||
23 | ${lib.optionalString cfg.no-tls "no-tls"} | ||
24 | ${lib.optionalString cfg.no-dtls "no-dtls"} | ||
25 | ${lib.optionalString cfg.no-udp-relay "no-udp-relay"} | ||
26 | ${lib.optionalString cfg.no-tcp-relay "no-tcp-relay"} | ||
27 | ${lib.optionalString (cfg.cert != null) "cert=${cfg.cert}"} | ||
28 | ${lib.optionalString (cfg.pkey != null) "pkey=${cfg.pkey}"} | ||
29 | ${lib.optionalString (cfg.dh-file != null) ("dh-file=${cfg.dh-file}")} | ||
30 | no-stdout-log | ||
31 | syslog | ||
32 | pidfile=${pidfile} | ||
33 | ${lib.optionalString cfg.secure-stun "secure-stun"} | ||
34 | ${lib.optionalString cfg.no-cli "no-cli"} | ||
35 | cli-ip=${cfg.cli-ip} | ||
36 | cli-port=${toString cfg.cli-port} | ||
37 | ${lib.optionalString (cfg.cli-password != null) ("cli-password=${cfg.cli-password}")} | ||
38 | ${cfg.extraConfig} | ||
39 | ''; | ||
40 | in { | ||
41 | disabledModules = [ "services/networking/coturn.nix" ]; | ||
42 | |||
43 | options = { | ||
44 | services.coturn = { | ||
45 | enable = mkEnableOption (lib.mdDoc "coturn TURN server"); | ||
46 | listening-port = mkOption { | ||
47 | type = types.int; | ||
48 | default = 3478; | ||
49 | description = lib.mdDoc '' | ||
50 | TURN listener port for UDP and TCP. | ||
51 | Note: actually, TLS and DTLS sessions can connect to the | ||
52 | "plain" TCP and UDP port(s), too - if allowed by configuration. | ||
53 | ''; | ||
54 | }; | ||
55 | tls-listening-port = mkOption { | ||
56 | type = types.int; | ||
57 | default = 5349; | ||
58 | description = lib.mdDoc '' | ||
59 | TURN listener port for TLS. | ||
60 | Note: actually, "plain" TCP and UDP sessions can connect to the TLS and | ||
61 | DTLS port(s), too - if allowed by configuration. The TURN server | ||
62 | "automatically" recognizes the type of traffic. Actually, two listening | ||
63 | endpoints (the "plain" one and the "tls" one) are equivalent in terms of | ||
64 | functionality; but we keep both endpoints to satisfy the RFC 5766 specs. | ||
65 | For secure TCP connections, we currently support SSL version 3 and | ||
66 | TLS version 1.0, 1.1 and 1.2. | ||
67 | For secure UDP connections, we support DTLS version 1. | ||
68 | ''; | ||
69 | }; | ||
70 | alt-listening-port = mkOption { | ||
71 | type = types.int; | ||
72 | default = cfg.listening-port + 1; | ||
73 | defaultText = literalExpression "listening-port + 1"; | ||
74 | description = lib.mdDoc '' | ||
75 | Alternative listening port for UDP and TCP listeners; | ||
76 | default (or zero) value means "listening port plus one". | ||
77 | This is needed for RFC 5780 support | ||
78 | (STUN extension specs, NAT behavior discovery). The TURN Server | ||
79 | supports RFC 5780 only if it is started with more than one | ||
80 | listening IP address of the same family (IPv4 or IPv6). | ||
81 | RFC 5780 is supported only by UDP protocol, other protocols | ||
82 | are listening to that endpoint only for "symmetry". | ||
83 | ''; | ||
84 | }; | ||
85 | alt-tls-listening-port = mkOption { | ||
86 | type = types.int; | ||
87 | default = cfg.tls-listening-port + 1; | ||
88 | defaultText = literalExpression "tls-listening-port + 1"; | ||
89 | description = lib.mdDoc '' | ||
90 | Alternative listening port for TLS and DTLS protocols. | ||
91 | ''; | ||
92 | }; | ||
93 | listening-ips = mkOption { | ||
94 | type = types.listOf types.str; | ||
95 | default = []; | ||
96 | example = [ "203.0.113.42" "2001:DB8::42" ]; | ||
97 | description = lib.mdDoc '' | ||
98 | Listener IP addresses of relay server. | ||
99 | If no IP(s) specified in the config file or in the command line options, | ||
100 | then all IPv4 and IPv6 system IPs will be used for listening. | ||
101 | ''; | ||
102 | }; | ||
103 | relay-ips = mkOption { | ||
104 | type = types.listOf types.str; | ||
105 | default = []; | ||
106 | example = [ "203.0.113.42" "2001:DB8::42" ]; | ||
107 | description = lib.mdDoc '' | ||
108 | Relay address (the local IP address that will be used to relay the | ||
109 | packets to the peer). | ||
110 | Multiple relay addresses may be used. | ||
111 | The same IP(s) can be used as both listening IP(s) and relay IP(s). | ||
112 | |||
113 | If no relay IP(s) specified, then the turnserver will apply the default | ||
114 | policy: it will decide itself which relay addresses to be used, and it | ||
115 | will always be using the client socket IP address as the relay IP address | ||
116 | of the TURN session (if the requested relay address family is the same | ||
117 | as the family of the client socket). | ||
118 | ''; | ||
119 | }; | ||
120 | min-port = mkOption { | ||
121 | type = types.int; | ||
122 | default = 49152; | ||
123 | description = lib.mdDoc '' | ||
124 | Lower bound of UDP relay endpoints | ||
125 | ''; | ||
126 | }; | ||
127 | max-port = mkOption { | ||
128 | type = types.int; | ||
129 | default = 65535; | ||
130 | description = lib.mdDoc '' | ||
131 | Upper bound of UDP relay endpoints | ||
132 | ''; | ||
133 | }; | ||
134 | lt-cred-mech = mkOption { | ||
135 | type = types.bool; | ||
136 | default = false; | ||
137 | description = lib.mdDoc '' | ||
138 | Use long-term credential mechanism. | ||
139 | ''; | ||
140 | }; | ||
141 | no-auth = mkOption { | ||
142 | type = types.bool; | ||
143 | default = false; | ||
144 | description = lib.mdDoc '' | ||
145 | This option is opposite to lt-cred-mech. | ||
146 | (TURN Server with no-auth option allows anonymous access). | ||
147 | If neither option is defined, and no users are defined, | ||
148 | then no-auth is default. If at least one user is defined, | ||
149 | in this file or in command line or in usersdb file, then | ||
150 | lt-cred-mech is default. | ||
151 | ''; | ||
152 | }; | ||
153 | use-auth-secret = mkOption { | ||
154 | type = types.bool; | ||
155 | default = false; | ||
156 | description = lib.mdDoc '' | ||
157 | TURN REST API flag. | ||
158 | Flag that sets a special authorization option that is based upon authentication secret. | ||
159 | This feature can be used with the long-term authentication mechanism, only. | ||
160 | This feature purpose is to support "TURN Server REST API", see | ||
161 | "TURN REST API" link in the project's page | ||
162 | https://github.com/coturn/coturn/ | ||
163 | |||
164 | This option is used with timestamp: | ||
165 | |||
166 | usercombo -> "timestamp:userid" | ||
167 | turn user -> usercombo | ||
168 | turn password -> base64(hmac(secret key, usercombo)) | ||
169 | |||
170 | This allows TURN credentials to be accounted for a specific user id. | ||
171 | If you don't have a suitable id, the timestamp alone can be used. | ||
172 | This option is just turning on secret-based authentication. | ||
173 | The actual value of the secret is defined either by option static-auth-secret, | ||
174 | or can be found in the turn_secret table in the database. | ||
175 | ''; | ||
176 | }; | ||
177 | static-auth-secret = mkOption { | ||
178 | type = types.nullOr types.str; | ||
179 | default = null; | ||
180 | description = lib.mdDoc '' | ||
181 | 'Static' authentication secret value (a string) for TURN REST API only. | ||
182 | If not set, then the turn server | ||
183 | will try to use the 'dynamic' value in turn_secret table | ||
184 | in user database (if present). The database-stored value can be changed on-the-fly | ||
185 | by a separate program, so this is why that other mode is 'dynamic'. | ||
186 | ''; | ||
187 | }; | ||
188 | static-auth-secret-file = mkOption { | ||
189 | type = types.nullOr types.str; | ||
190 | default = null; | ||
191 | description = lib.mdDoc '' | ||
192 | Path to the file containing the static authentication secret. | ||
193 | ''; | ||
194 | }; | ||
195 | realm = mkOption { | ||
196 | type = types.str; | ||
197 | default = config.networking.hostName; | ||
198 | defaultText = literalExpression "config.networking.hostName"; | ||
199 | example = "example.com"; | ||
200 | description = lib.mdDoc '' | ||
201 | The default realm to be used for the users when no explicit | ||
202 | origin/realm relationship was found in the database, or if the TURN | ||
203 | server is not using any database (just the commands-line settings | ||
204 | and the userdb file). Must be used with long-term credentials | ||
205 | mechanism or with TURN REST API. | ||
206 | ''; | ||
207 | }; | ||
208 | cert = mkOption { | ||
209 | type = types.nullOr types.str; | ||
210 | default = null; | ||
211 | example = "/var/lib/acme/example.com/fullchain.pem"; | ||
212 | description = lib.mdDoc '' | ||
213 | Certificate file in PEM format. | ||
214 | ''; | ||
215 | }; | ||
216 | pkey = mkOption { | ||
217 | type = types.nullOr types.str; | ||
218 | default = null; | ||
219 | example = "/var/lib/acme/example.com/key.pem"; | ||
220 | description = lib.mdDoc '' | ||
221 | Private key file in PEM format. | ||
222 | ''; | ||
223 | }; | ||
224 | dh-file = mkOption { | ||
225 | type = types.nullOr types.str; | ||
226 | default = null; | ||
227 | description = lib.mdDoc '' | ||
228 | Use custom DH TLS key, stored in PEM format in the file. | ||
229 | ''; | ||
230 | }; | ||
231 | secure-stun = mkOption { | ||
232 | type = types.bool; | ||
233 | default = false; | ||
234 | description = lib.mdDoc '' | ||
235 | Require authentication of the STUN Binding request. | ||
236 | By default, the clients are allowed anonymous access to the STUN Binding functionality. | ||
237 | ''; | ||
238 | }; | ||
239 | no-cli = mkOption { | ||
240 | type = types.bool; | ||
241 | default = false; | ||
242 | description = lib.mdDoc '' | ||
243 | Turn OFF the CLI support. | ||
244 | ''; | ||
245 | }; | ||
246 | cli-ip = mkOption { | ||
247 | type = types.str; | ||
248 | default = "127.0.0.1"; | ||
249 | description = lib.mdDoc '' | ||
250 | Local system IP address to be used for CLI server endpoint. | ||
251 | ''; | ||
252 | }; | ||
253 | cli-port = mkOption { | ||
254 | type = types.int; | ||
255 | default = 5766; | ||
256 | description = lib.mdDoc '' | ||
257 | CLI server port. | ||
258 | ''; | ||
259 | }; | ||
260 | cli-password = mkOption { | ||
261 | type = types.nullOr types.str; | ||
262 | default = null; | ||
263 | description = lib.mdDoc '' | ||
264 | CLI access password. | ||
265 | For the security reasons, it is recommended to use the encrypted | ||
266 | for of the password (see the -P command in the turnadmin utility). | ||
267 | ''; | ||
268 | }; | ||
269 | no-udp = mkOption { | ||
270 | type = types.bool; | ||
271 | default = false; | ||
272 | description = lib.mdDoc "Disable UDP client listener"; | ||
273 | }; | ||
274 | no-tcp = mkOption { | ||
275 | type = types.bool; | ||
276 | default = false; | ||
277 | description = lib.mdDoc "Disable TCP client listener"; | ||
278 | }; | ||
279 | no-tls = mkOption { | ||
280 | type = types.bool; | ||
281 | default = false; | ||
282 | description = lib.mdDoc "Disable TLS client listener"; | ||
283 | }; | ||
284 | no-dtls = mkOption { | ||
285 | type = types.bool; | ||
286 | default = false; | ||
287 | description = lib.mdDoc "Disable DTLS client listener"; | ||
288 | }; | ||
289 | no-udp-relay = mkOption { | ||
290 | type = types.bool; | ||
291 | default = false; | ||
292 | description = lib.mdDoc "Disable UDP relay endpoints"; | ||
293 | }; | ||
294 | no-tcp-relay = mkOption { | ||
295 | type = types.bool; | ||
296 | default = false; | ||
297 | description = lib.mdDoc "Disable TCP relay endpoints"; | ||
298 | }; | ||
299 | extraConfig = mkOption { | ||
300 | type = types.lines; | ||
301 | default = ""; | ||
302 | description = lib.mdDoc "Additional configuration options"; | ||
303 | }; | ||
304 | }; | ||
305 | }; | ||
306 | |||
307 | config = mkIf cfg.enable (mkMerge ([ | ||
308 | { assertions = [ | ||
309 | { assertion = cfg.static-auth-secret != null -> cfg.static-auth-secret-file == null ; | ||
310 | message = "static-auth-secret and static-auth-secret-file cannot be set at the same time"; | ||
311 | } | ||
312 | ];} | ||
313 | |||
314 | { | ||
315 | users.users.turnserver = | ||
316 | { uid = config.ids.uids.turnserver; | ||
317 | group = "turnserver"; | ||
318 | description = "coturn TURN server user"; | ||
319 | }; | ||
320 | users.groups.turnserver = | ||
321 | { gid = config.ids.gids.turnserver; | ||
322 | members = [ "turnserver" ]; | ||
323 | }; | ||
324 | |||
325 | systemd.services.coturn = let | ||
326 | runConfig = "/run/coturn/turnserver.cfg"; | ||
327 | in { | ||
328 | description = "coturn TURN server"; | ||
329 | after = [ "network-online.target" ]; | ||
330 | wants = [ "network-online.target" ]; | ||
331 | wantedBy = [ "multi-user.target" ]; | ||
332 | |||
333 | unitConfig = { | ||
334 | Documentation = "man:coturn(1) man:turnadmin(1) man:turnserver(1)"; | ||
335 | }; | ||
336 | |||
337 | script = '' | ||
338 | cat ${configFile} > ${runConfig} | ||
339 | ${optionalString (cfg.static-auth-secret-file != null) '' | ||
340 | ${pkgs.replace-secret}/bin/replace-secret \ | ||
341 | "#static-auth-secret#" \ | ||
342 | ${cfg.static-auth-secret-file} \ | ||
343 | ${runConfig} | ||
344 | '' } | ||
345 | chmod 640 ${runConfig} | ||
346 | |||
347 | exec ${pkgs.coturn}/bin/turnserver -c ${runConfig} | ||
348 | ''; | ||
349 | serviceConfig = { | ||
350 | Type = "simple"; | ||
351 | RuntimeDirectory = "turnserver"; | ||
352 | User = "turnserver"; | ||
353 | Group = "turnserver"; | ||
354 | AmbientCapabilities = | ||
355 | mkIf ( | ||
356 | cfg.listening-port < 1024 || | ||
357 | cfg.alt-listening-port < 1024 || | ||
358 | cfg.tls-listening-port < 1024 || | ||
359 | cfg.alt-tls-listening-port < 1024 || | ||
360 | cfg.min-port < 1024 | ||
361 | ) "cap_net_bind_service"; | ||
362 | Restart = "on-abort"; | ||
363 | }; | ||
364 | }; | ||
365 | systemd.tmpfiles.rules = [ | ||
366 | "d /run/coturn 0700 turnserver turnserver - -" | ||
367 | ]; | ||
368 | }])); | ||
369 | } | ||
diff --git a/modules/zfssnap/default.nix b/modules/zfssnap/default.nix index f6f32852..23041c36 100644 --- a/modules/zfssnap/default.nix +++ b/modules/zfssnap/default.nix | |||
@@ -27,19 +27,27 @@ in { | |||
27 | enable = mkEnableOption "zfssnap service"; | 27 | enable = mkEnableOption "zfssnap service"; |
28 | 28 | ||
29 | config = mkOption { | 29 | config = mkOption { |
30 | type = with types; attrsOf (attrsOf str); | 30 | type = types.submodule { |
31 | default = { | 31 | options = { |
32 | keep = { | 32 | keep = mkOption { |
33 | within = "15m"; | 33 | type = with types; attrsOf str; |
34 | "5m" = "48"; | 34 | default = { |
35 | "15m" = "32"; | 35 | within = "15m"; |
36 | hourly = "48"; | 36 | "5m" = "48"; |
37 | "4h" = "24"; | 37 | "15m" = "32"; |
38 | "12h" = "12"; | 38 | hourly = "48"; |
39 | daily = "62"; | 39 | "4h" = "24"; |
40 | halfweekly = "32"; | 40 | "12h" = "12"; |
41 | weekly = "24"; | 41 | daily = "62"; |
42 | monthly = "-1"; | 42 | halfweekly = "32"; |
43 | weekly = "24"; | ||
44 | monthly = "-1"; | ||
45 | }; | ||
46 | }; | ||
47 | exec = mkOption { | ||
48 | type = with types; attrsOf str; | ||
49 | default = {}; | ||
50 | }; | ||
43 | }; | 51 | }; |
44 | }; | 52 | }; |
45 | }; | 53 | }; |