]> Git — Sourcephile - julm/julm-nix.git/blob - nixpkgs/patches/syncoid/0002-nixos-syncoid-use-DynamicUser.patch
nix: update to nixos-25.05
[julm/julm-nix.git] / nixpkgs / patches / syncoid / 0002-nixos-syncoid-use-DynamicUser.patch
1 From 0e837ecd5ffb5e8366a5e2878838ba691b31cd99 Mon Sep 17 00:00:00 2001
2 From: Julien Moutinho <julm+nixpkgs@sourcephile.fr>
3 Date: Sat, 27 Nov 2021 02:50:39 +0100
4 Subject: [PATCH 2/2] nixos/syncoid: use DynamicUser=
5
6 HistoryNote: the following changes
7 were previously in separate commits
8 but have been squashed and nixfmt-rfc-style
9 on be able to rebase them
10 onto a treewide change applying nixfmt-rfc-style
11 on nixos/modules/services/backup/syncoid.nix
12 which introduced multiple untrackable conflicts.
13
14 Changes:
15
16 - nixos/syncoid: add destroy to localTargetAllow's default
17
18 syncoid[3282027]: Resuming interrupted zfs send/receive from rpool/var/mail to losurdo/backup/mermet/var/mail (~ UNKNOWN remaining):
19 syncoid[3282885]: cannot resume send: 'rpool/var/mail@autosnap_2021-10-17_15:00:19_hourly' used in the initial send no longer exists
20 syncoid[3282897]: cannot receive: failed to read from stream
21 syncoid[3282027]: WARN: resetting partially receive state because the snapshot source no longer exists
22 syncoid[3283326]: cannot destroy 'losurdo/backup/mermet/var/mail/%recv': permission denied
23
24 - nixos/syncoid: allow @timer syscalls
25
26 - nixos/syncoid: set TZ envvar
27
28 - nixos/syncoid: test sending to multiple targets
29
30 - nixos/syncoid: don't delegate permissions to source's parent
31
32 See https://github.com/NixOS/nixpkgs/pull/165204
33 and https://github.com/NixOS/nixpkgs/pull/180111
34
35 - nixos/syncoid: add support for LoadCredentialEncrypted=
36
37 - nixos/syncoid: zfs-unallow unused dynamic users
38
39 - nixos/syncoid: use NFTSet=
40
41 Changes between the treewide nixfmt-rfc-style
42 and those changes have been preserved:
43
44 - 71967c47e54c56557c0ee7bec7fe554c018e37c4 nixos/syncoid: allow interval to be list of strings
45 - f34483be5ee2418a563545a56743b7b59c549935 nixosTests: handleTest -> runTest, batch 1
46 ---
47 nixos/modules/services/backup/syncoid.nix | 454 +++++++++++-----------
48 nixos/tests/sanoid.nix | 78 ++--
49 2 files changed, 269 insertions(+), 263 deletions(-)
50
51 diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
52 index 8b4c59155f..7d344a1ad0 100644
53 --- a/nixos/modules/services/backup/syncoid.nix
54 +++ b/nixos/modules/services/backup/syncoid.nix
55 @@ -7,7 +7,7 @@
56 let
57 cfg = config.services.syncoid;
58
59 - # Extract local dasaset names (so no datasets containing "@")
60 + # Extract local dataset names (so no datasets containing "@")
61 localDatasetName =
62 d:
63 lib.optionals (d != null) (
64 @@ -20,81 +20,9 @@ let
65 # Escape as required by: https://www.freedesktop.org/software/systemd/man/systemd.unit.html
66 escapeUnitName =
67 name:
68 - lib.concatMapStrings (s: if lib.isList s then "-" else s) (
69 + lib.concatMapStrings (s: if builtins.isList s then "-" else s) (
70 builtins.split "[^a-zA-Z0-9_.\\-]+" name
71 );
72 -
73 - # Function to build "zfs allow" commands for the filesystems we've delegated
74 - # permissions to. It also checks if the target dataset exists before
75 - # delegating permissions, if it doesn't exist we delegate it to the parent
76 - # dataset (if it exists). This should solve the case of provisoning new
77 - # datasets.
78 - buildAllowCommand =
79 - permissions: dataset:
80 - (
81 - "-+${pkgs.writeShellScript "zfs-allow-${dataset}" ''
82 - # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
83 -
84 - # Run a ZFS list on the dataset to check if it exists
85 - if ${
86 - lib.escapeShellArgs [
87 - "/run/booted-system/sw/bin/zfs"
88 - "list"
89 - dataset
90 - ]
91 - } 2> /dev/null; then
92 - ${lib.escapeShellArgs [
93 - "/run/booted-system/sw/bin/zfs"
94 - "allow"
95 - cfg.user
96 - (lib.concatStringsSep "," permissions)
97 - dataset
98 - ]}
99 - ${lib.optionalString ((builtins.dirOf dataset) != ".") ''
100 - else
101 - ${lib.escapeShellArgs [
102 - "/run/booted-system/sw/bin/zfs"
103 - "allow"
104 - cfg.user
105 - (lib.concatStringsSep "," permissions)
106 - # Remove the last part of the path
107 - (builtins.dirOf dataset)
108 - ]}
109 - ''}
110 - fi
111 - ''}"
112 - );
113 -
114 - # Function to build "zfs unallow" commands for the filesystems we've
115 - # delegated permissions to. Here we unallow both the target but also
116 - # on the parent dataset because at this stage we have no way of
117 - # knowing if the allow command did execute on the parent dataset or
118 - # not in the pre-hook. We can't run the same if in the post hook
119 - # since the dataset should have been created at this point.
120 - buildUnallowCommand =
121 - permissions: dataset:
122 - (
123 - "-+${pkgs.writeShellScript "zfs-unallow-${dataset}" ''
124 - # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
125 - ${lib.escapeShellArgs [
126 - "/run/booted-system/sw/bin/zfs"
127 - "unallow"
128 - cfg.user
129 - (lib.concatStringsSep "," permissions)
130 - dataset
131 - ]}
132 - ${lib.optionalString ((builtins.dirOf dataset) != ".") (
133 - lib.escapeShellArgs [
134 - "/run/booted-system/sw/bin/zfs"
135 - "unallow"
136 - cfg.user
137 - (lib.concatStringsSep "," permissions)
138 - # Remove the last part of the path
139 - (builtins.dirOf dataset)
140 - ]
141 - )}
142 - ''}"
143 - );
144 in
145 {
146
147 @@ -120,38 +48,23 @@ in
148 '';
149 };
150
151 - user = lib.mkOption {
152 - type = lib.types.str;
153 - default = "syncoid";
154 - example = "backup";
155 - description = ''
156 - The user for the service. ZFS privilege delegation will be
157 - automatically configured for any local pools used by syncoid if this
158 - option is set to a user other than root. The user will be given the
159 - "hold" and "send" privileges on any pool that has datasets being sent
160 - and the "create", "mount", "receive", and "rollback" privileges on
161 - any pool that has datasets being received.
162 - '';
163 - };
164 -
165 - group = lib.mkOption {
166 - type = lib.types.str;
167 - default = "syncoid";
168 - example = "backup";
169 - description = "The group for the service.";
170 - };
171 -
172 sshKey = lib.mkOption {
173 type = with lib.types; nullOr (coercedTo path toString str);
174 default = null;
175 description = ''
176 - SSH private key file to use to login to the remote system. Can be
177 - overridden in individual commands.
178 + SSH private key file to use to login to the remote system.
179 + It can be overridden in individual commands.
180 + It is loaded using `LoadCredentialEncrypted=`
181 + when its path is prefixed by a credential name and colon,
182 + otherwise `LoadCredential=` is used.
183 + For more SSH tuning, you may use syncoid's `--sshoption`
184 + in {option}`services.syncoid.commonArgs`
185 + and/or in the `extraArgs` of a specific command.
186 '';
187 };
188
189 localSourceAllow = lib.mkOption {
190 - type = lib.types.listOf lib.types.str;
191 + type = with lib.types; listOf str;
192 # Permissions snapshot and destroy are in case --no-sync-snap is not used
193 default = [
194 "bookmark"
195 @@ -162,19 +75,22 @@ in
196 "mount"
197 ];
198 description = ''
199 - Permissions granted for the {option}`services.syncoid.user` user
200 - for local source datasets. See
201 - <https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html>
202 + Permissions granted for the syncoid user for local source datasets.
203 + See <https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html>
204 for available permissions.
205 '';
206 };
207
208 localTargetAllow = lib.mkOption {
209 - type = lib.types.listOf lib.types.str;
210 + type = with lib.types; listOf str;
211 + # Permission destroy is required to reset broken receive states (zfs receive -A),
212 + # which syncoid does when it fails to resume a receive state,
213 + # when the snapshot it refers to has been destroyed on the source.
214 default = [
215 "change-key"
216 "compression"
217 "create"
218 + "destroy"
219 "mount"
220 "mountpoint"
221 "receive"
222 @@ -187,9 +103,8 @@ in
223 "rollback"
224 ];
225 description = ''
226 - Permissions granted for the {option}`services.syncoid.user` user
227 - for local target datasets. See
228 - <https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html>
229 + Permissions granted for the syncoid user for local target datasets.
230 + See <https://openzfs.github.io/openzfs-docs/man/8/zfs-allow.8.html>
231 for available permissions.
232 Make sure to include the `change-key` permission if you send raw encrypted datasets,
233 the `compression` permission if you send raw compressed datasets, and so on.
234 @@ -198,7 +113,7 @@ in
235 };
236
237 commonArgs = lib.mkOption {
238 - type = lib.types.listOf lib.types.str;
239 + type = with lib.types; listOf str;
240 default = [ ];
241 example = [ "--no-sync-snap" ];
242 description = ''
243 @@ -296,13 +211,13 @@ in
244 '';
245 };
246
247 - useCommonArgs = lib.mkOption {
248 - type = lib.types.bool;
249 - default = true;
250 - description = ''
251 - Whether to add the configured common arguments to this command.
252 - '';
253 - };
254 + useCommonArgs =
255 + lib.mkEnableOption ''
256 + configured common arguments to this command
257 + ''
258 + // {
259 + default = true;
260 + };
261
262 service = lib.mkOption {
263 type = lib.types.attrs;
264 @@ -341,139 +256,216 @@ in
265 # Implementation
266
267 config = lib.mkIf cfg.enable {
268 - users = {
269 - users = lib.mkIf (cfg.user == "syncoid") {
270 - syncoid = {
271 - group = cfg.group;
272 - isSystemUser = true;
273 - # For syncoid to be able to create /var/lib/syncoid/.ssh/
274 - # and to use custom ssh_config or known_hosts.
275 - home = "/var/lib/syncoid";
276 - createHome = false;
277 - };
278 - };
279 - groups = lib.mkIf (cfg.group == "syncoid") {
280 - syncoid = { };
281 - };
282 - };
283 -
284 systemd.services = lib.mapAttrs' (
285 name: c:
286 + let
287 + sshKeyCred = builtins.split ":" c.sshKey;
288 + in
289 lib.nameValuePair "syncoid-${escapeUnitName name}" (
290 lib.mkMerge [
291 {
292 description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}";
293 after = [ "zfs.target" ];
294 startAt = cfg.interval;
295 - # syncoid may need zpool to get feature@extensible_dataset
296 - path = [ "/run/booted-system/sw/bin/" ];
297 - serviceConfig = {
298 - ExecStartPre =
299 - (map (buildAllowCommand c.localSourceAllow) (localDatasetName c.source))
300 - ++ (map (buildAllowCommand c.localTargetAllow) (localDatasetName c.target));
301 - ExecStopPost =
302 - (map (buildUnallowCommand c.localSourceAllow) (localDatasetName c.source))
303 - ++ (map (buildUnallowCommand c.localTargetAllow) (localDatasetName c.target));
304 - ExecStart = lib.escapeShellArgs (
305 - [ "${cfg.package}/bin/syncoid" ]
306 - ++ lib.optionals c.useCommonArgs cfg.commonArgs
307 - ++ lib.optional c.recursive "-r"
308 - ++ lib.optionals (c.sshKey != null) [
309 - "--sshkey"
310 - c.sshKey
311 - ]
312 - ++ c.extraArgs
313 - ++ [
314 - "--sendoptions"
315 - c.sendOptions
316 - "--recvoptions"
317 - c.recvOptions
318 - "--no-privilege-elevation"
319 - c.source
320 - c.target
321 - ]
322 - );
323 - User = cfg.user;
324 - Group = cfg.group;
325 - StateDirectory = [ "syncoid" ];
326 - StateDirectoryMode = "700";
327 - # Prevent SSH control sockets of different syncoid services from interfering
328 - PrivateTmp = true;
329 - # Permissive access to /proc because syncoid
330 - # calls ps(1) to detect ongoing `zfs receive`.
331 - ProcSubset = "all";
332 - ProtectProc = "default";
333 + # Here we explicitly use the booted system to guarantee the stable API needed by ZFS.
334 + # Moreover syncoid may need zpool to get feature@extensible_dataset.
335 + path = [ "/run/booted-system/sw" ];
336 + # Prevents missing snapshots during DST changes
337 + environment.TZ = "UTC";
338 + # A custom LD_LIBRARY_PATH is needed to access in `getent passwd`
339 + # the systemd's entry about the DynamicUser=,
340 + # so that ssh won't fail with: "No user exists for uid $UID".
341 + environment.LD_LIBRARY_PATH = config.system.nssModules.path;
342 + serviceConfig =
343 + {
344 + ExecStartPre =
345 + # Recursively remove any residual permissions
346 + # given on local+descendant datasets (source, target or target's parent)
347 + # to any currently unknown (hence unused) systemd dynamic users (UID/GID range 61184…65519),
348 + # which happens when a crash has occurred
349 + # during any previous run of a syncoid-*.service (not only this one).
350 + map
351 + (
352 + dataset:
353 + "+"
354 + + pkgs.writeShellScript "zfs-unallow-unused-dynamic-users" ''
355 + set -eu
356 + zfs allow "$1" |
357 + sed -ne 's/^\t\(user\|group\) (unknown: \([0-9]\+\)).*/\1 \2/p' |
358 + {
359 + declare -a uids
360 + while read -r role id; do
361 + if [ "$id" -ge 61184 ] && [ "$id" -le 65519 ]; then
362 + case "$role" in
363 + (user) uids+=("$id");;
364 + esac
365 + fi
366 + done
367 + zfs unallow -r -u "$(printf %s, "''${uids[@]}")" "$1"
368 + }
369 + ''
370 + + " "
371 + + lib.escapeShellArg dataset
372 + )
373 + (
374 + localDatasetName c.source
375 + ++ localDatasetName c.target
376 + ++ map builtins.dirOf (localDatasetName c.target)
377 + )
378 + ++
379 + # For a local source, allow the localSourceAllow ZFS permissions.
380 + map (
381 + dataset:
382 + "+/run/booted-system/sw/bin/zfs allow $USER "
383 + + lib.escapeShellArgs [
384 + (lib.concatStringsSep "," c.localSourceAllow)
385 + dataset
386 + ]
387 + ) (localDatasetName c.source)
388 + ++
389 + # For a local target, check if the dataset exists before delegating permissions,
390 + # and if it doesn't exist, delegate it to the parent dataset.
391 + # This should solve the case of provisioning new datasets.
392 + map (
393 + dataset:
394 + "+"
395 + + pkgs.writeShellScript "zfs-allow-target" ''
396 + dataset="$1"
397 + # Run a ZFS list on the dataset to check if it exists
398 + zfs list "$dataset" >/dev/null 2>/dev/null ||
399 + dataset="$(dirname "$dataset")"
400 + zfs allow "$USER" ${lib.escapeShellArg (lib.concatStringsSep "," c.localTargetAllow)} "$dataset"
401 + ''
402 + + " "
403 + + lib.escapeShellArg dataset
404 + ) (localDatasetName c.target);
405 + ExecStopPost =
406 + let
407 + zfsUnallow = dataset: "+/run/booted-system/sw/bin/zfs unallow $USER " + lib.escapeShellArg dataset;
408 + in
409 + map zfsUnallow (localDatasetName c.source)
410 + ++
411 + # For a local target, unallow both the dataset and its parent,
412 + # because at this stage we have no way of knowing if the allow command
413 + # did execute on the parent dataset or not in the ExecStartPre=.
414 + # We can't run the same if-then-else in the post hook
415 + # since the dataset should have been created at this point.
416 + lib.concatMap (dataset: [
417 + (zfsUnallow dataset)
418 + (zfsUnallow (builtins.dirOf dataset))
419 + ]) (localDatasetName c.target);
420 + ExecStart = lib.escapeShellArgs (
421 + [ "${cfg.package}/bin/syncoid" ]
422 + ++ lib.optionals c.useCommonArgs cfg.commonArgs
423 + ++ lib.optional c.recursive "--recursive"
424 + ++ lib.optionals (c.sshKey != null) [
425 + "--sshkey"
426 + "\${CREDENTIALS_DIRECTORY}/${if lib.length sshKeyCred > 1 then lib.head sshKeyCred else "sshKey"}"
427 + ]
428 + ++ c.extraArgs
429 + ++ [
430 + "--sendoptions"
431 + c.sendOptions
432 + "--recvoptions"
433 + c.recvOptions
434 + "--no-privilege-elevation"
435 + c.source
436 + c.target
437 + ]
438 + );
439 + DynamicUser = true;
440 + NFTSet = lib.optional config.networking.nftables.enable "user:inet:filter:nixos_syncoid_uids";
441 + # Prevent SSH control sockets of different syncoid services from interfering
442 + PrivateTmp = true;
443 + # Permissive access to /proc because syncoid
444 + # calls ps(1) to detect ongoing `zfs receive`.
445 + ProcSubset = "all";
446 + ProtectProc = "default";
447
448 - # The following options are only for optimizing:
449 - # systemd-analyze security | grep syncoid-'*'
450 - AmbientCapabilities = "";
451 - CapabilityBoundingSet = "";
452 - DeviceAllow = [ "/dev/zfs" ];
453 - LockPersonality = true;
454 - MemoryDenyWriteExecute = true;
455 - NoNewPrivileges = true;
456 - PrivateDevices = true;
457 - PrivateMounts = true;
458 - PrivateNetwork = lib.mkDefault false;
459 - PrivateUsers = false; # Enabling this breaks on zfs-2.2.0
460 - ProtectClock = true;
461 - ProtectControlGroups = true;
462 - ProtectHome = true;
463 - ProtectHostname = true;
464 - ProtectKernelLogs = true;
465 - ProtectKernelModules = true;
466 - ProtectKernelTunables = true;
467 - ProtectSystem = "strict";
468 - RemoveIPC = true;
469 - RestrictAddressFamilies = [
470 - "AF_UNIX"
471 - "AF_INET"
472 - "AF_INET6"
473 - ];
474 - RestrictNamespaces = true;
475 - RestrictRealtime = true;
476 - RestrictSUIDSGID = true;
477 - RootDirectory = "/run/syncoid/${escapeUnitName name}";
478 - RootDirectoryStartOnly = true;
479 - BindPaths = [ "/dev/zfs" ];
480 - BindReadOnlyPaths = [
481 - builtins.storeDir
482 - "/etc"
483 - "/run"
484 - "/bin/sh"
485 - ];
486 - # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
487 - InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
488 - MountAPIVFS = true;
489 - # Create RootDirectory= in the host's mount namespace.
490 - RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
491 - RuntimeDirectoryMode = "700";
492 - SystemCallFilter = [
493 - "@system-service"
494 - # Groups in @system-service which do not contain a syscall listed by:
495 - # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
496 - # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
497 - # systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' '
498 - "~@aio"
499 - "~@chown"
500 - "~@keyring"
501 - "~@memlock"
502 - "~@privileged"
503 - "~@resources"
504 - "~@setuid"
505 - "~@timer"
506 - ];
507 - SystemCallArchitectures = "native";
508 - # This is for BindPaths= and BindReadOnlyPaths=
509 - # to allow traversal of directories they create in RootDirectory=.
510 - UMask = "0066";
511 - };
512 + # The following options are only for optimizing:
513 + # systemd-analyze security | grep syncoid-'*'
514 + AmbientCapabilities = "";
515 + CapabilityBoundingSet = "";
516 + DeviceAllow = [ "/dev/zfs" ];
517 + LockPersonality = true;
518 + MemoryDenyWriteExecute = true;
519 + NoNewPrivileges = true;
520 + PrivateDevices = true;
521 + PrivateMounts = true;
522 + PrivateNetwork = lib.mkDefault false;
523 + PrivateUsers = false; # Enabling this breaks on zfs-2.2.0
524 + ProtectClock = true;
525 + ProtectControlGroups = true;
526 + ProtectHome = true;
527 + ProtectHostname = true;
528 + ProtectKernelLogs = true;
529 + ProtectKernelModules = true;
530 + ProtectKernelTunables = true;
531 + ProtectSystem = "strict";
532 + RemoveIPC = true;
533 + RestrictAddressFamilies = [
534 + "AF_UNIX"
535 + "AF_INET"
536 + "AF_INET6"
537 + ];
538 + RestrictNamespaces = true;
539 + RestrictRealtime = true;
540 + RestrictSUIDSGID = true;
541 + RootDirectory = "/run/syncoid/${escapeUnitName name}";
542 + BindPaths = [ "/dev/zfs" ];
543 + BindReadOnlyPaths = [
544 + builtins.storeDir
545 + "/etc"
546 + "/run"
547 + # Some programs hardcode /var/run
548 + # eg. /var/run/avahi-daemon/socket to resolve *.local mDNS domains
549 + "/var/run"
550 + "/bin/sh"
551 + ];
552 + # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
553 + InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
554 + MountAPIVFS = true;
555 + # Create RootDirectory= in the host's mount namespace.
556 + RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
557 + RuntimeDirectoryMode = "700";
558 + SystemCallFilter = [
559 + "@system-service"
560 + # Groups in @system-service which do not contain a syscall listed by:
561 + # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
562 + # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
563 + # systemd-analyze syscall-filter | grep -v -e '#' | sed -e ':loop; /^[^ ]/N; s/\n //; t loop' | grep $(printf ' -e \\<%s\\>' $(cat perf.syscalls)) | cut -f 1 -d ' '
564 + "~@aio"
565 + "~@chown"
566 + "~@keyring"
567 + "~@memlock"
568 + "~@privileged"
569 + "~@resources"
570 + "~@setuid"
571 + ];
572 + SystemCallArchitectures = "native";
573 + # This is for BindPaths= and BindReadOnlyPaths=
574 + # to allow traversal of directories they create in RootDirectory=.
575 + UMask = "0066";
576 + }
577 + // (
578 + if lib.length sshKeyCred > 1 then
579 + { LoadCredentialEncrypted = [ c.sshKey ]; }
580 + else
581 + { LoadCredential = [ "sshKey:${c.sshKey}" ]; }
582 + );
583 }
584 cfg.service
585 c.service
586 ]
587 )
588 ) cfg.commands;
589 +
590 + networking.nftables.ruleset = lib.mkBefore ''
591 + table inet filter {
592 + # A set containing the dynamic UIDs of the syncoid services currently active
593 + set nixos_syncoid_uids { typeof meta skuid; }
594 + }
595 + '';
596 };
597
598 meta.maintainers = with lib.maintainers; [
599 diff --git a/nixos/tests/sanoid.nix b/nixos/tests/sanoid.nix
600 index e42fd54dfd..eabe88b6be 100644
601 --- a/nixos/tests/sanoid.nix
602 +++ b/nixos/tests/sanoid.nix
603 @@ -50,16 +50,26 @@ in
604 enable = true;
605 sshKey = "/var/lib/syncoid/id_ecdsa";
606 commands = {
607 - # Sync snapshot taken by sanoid
608 - "pool/sanoid" = {
609 - target = "root@target:pool/sanoid";
610 + # Take snapshot and sync
611 + "pool/syncoid".target = "root@target:pool/syncoid";
612 +
613 + # Sync the same dataset to different targets
614 + "pool/sanoid1" = {
615 + source = "pool/sanoid";
616 + target = "root@target:pool/sanoid1";
617 + extraArgs = [
618 + "--no-sync-snap"
619 + "--create-bookmark"
620 + ];
621 + };
622 + "pool/sanoid2" = {
623 + source = "pool/sanoid";
624 + target = "root@target:pool/sanoid2";
625 extraArgs = [
626 "--no-sync-snap"
627 "--create-bookmark"
628 ];
629 };
630 - # Take snapshot and sync
631 - "pool/syncoid".target = "root@target:pool/syncoid";
632
633 # Test pool without parent (regression test for https://github.com/NixOS/nixpkgs/pull/180111)
634 "pool".target = "root@target:pool/full-pool";
635 @@ -94,6 +104,7 @@ in
636 "zfs create pool/syncoid",
637 "udevadm settle",
638 )
639 +
640 target.succeed(
641 "mkdir /mnt",
642 "parted --script /dev/vdb -- mklabel msdos mkpart primary 1024M -1s",
643 @@ -106,41 +117,44 @@ in
644 "mkdir -m 700 -p /var/lib/syncoid",
645 "cat '${snakeOilPrivateKey}' > /var/lib/syncoid/id_ecdsa",
646 "chmod 600 /var/lib/syncoid/id_ecdsa",
647 - "chown -R syncoid:syncoid /var/lib/syncoid/",
648 )
649
650 - assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set before snapshotting"
651 - assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set before snapshotting"
652 - assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set before snapshotting"
653 + with subtest("Take snapshots with sanoid"):
654 + source.succeed("touch /mnt/pool/sanoid/test.txt")
655 + source.succeed("touch /mnt/pool/compat/test.txt")
656 + source.systemctl("start --wait sanoid.service")
657
658 - # Take snapshot with sanoid
659 - source.succeed("touch /mnt/pool/sanoid/test.txt")
660 - source.succeed("touch /mnt/pool/compat/test.txt")
661 - source.systemctl("start --wait sanoid.service")
662 + # Add some unused dynamic users to the stateful allow list of ZFS datasets,
663 + # simulating a state where they remain after the system crashed,
664 + # to check they'll be correctly removed by the syncoid services.
665 + # Each syncoid service run from now may reuse at most one of them for itself.
666 + source.succeed(
667 + "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool",
668 + "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool/sanoid",
669 + "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool/syncoid",
670 + )
671 +
672 + with subtest("sync snapshots"):
673 + target.wait_for_open_port(22)
674 + source.succeed("touch /mnt/pool/syncoid/test.txt")
675 +
676 + source.systemctl("start --wait syncoid-pool-syncoid.service")
677 + target.succeed("cat /mnt/pool/syncoid/test.txt")
678 +
679 + source.systemctl("start --wait syncoid-pool-sanoid{1,2}.service")
680 + target.succeed("cat /mnt/pool/sanoid1/test.txt")
681 + target.succeed("cat /mnt/pool/sanoid2/test.txt")
682 +
683 + source.systemctl("start --wait syncoid-pool.service")
684 + target.succeed("[[ -d /mnt/pool/full-pool/syncoid ]]")
685 +
686 + source.systemctl("start --wait syncoid-pool-compat.service")
687 + target.succeed("cat /mnt/pool/compat/test.txt")
688
689 assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after snapshotting"
690 assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after snapshotting"
691 assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after snapshotting"
692 -
693 - # Sync snapshots
694 - target.wait_for_open_port(22)
695 - source.succeed("touch /mnt/pool/syncoid/test.txt")
696 - source.systemctl("start --wait syncoid-pool-sanoid.service")
697 - target.succeed("cat /mnt/pool/sanoid/test.txt")
698 - source.systemctl("start --wait syncoid-pool-syncoid.service")
699 - source.systemctl("start --wait syncoid-pool-syncoid.service")
700 - target.succeed("cat /mnt/pool/syncoid/test.txt")
701 -
702 assert(len(source.succeed("zfs list -H -t snapshot pool/syncoid").splitlines()) == 1), "Syncoid should only retain one sync snapshot"
703
704 - source.systemctl("start --wait syncoid-pool.service")
705 - target.succeed("[[ -d /mnt/pool/full-pool/syncoid ]]")
706 -
707 - source.systemctl("start --wait syncoid-pool-compat.service")
708 - target.succeed("cat /mnt/pool/compat/test.txt")
709 -
710 - assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after syncing snapshots"
711 - assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after syncing snapshots"
712 - assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after syncing snapshots"
713 '';
714 }
715 --
716 2.47.2
717