]> Git — Sourcephile - julm/julm-nix.git/blob - nixpkgs/patches/syncoid/0002-nixos-syncoid-use-DynamicUser.patch
nix: update nixpkgs-unstable
[julm/julm-nix.git] / nixpkgs / patches / syncoid / 0002-nixos-syncoid-use-DynamicUser.patch
1 From 41d06042cb9c2d4070a732745305b85b6328b58f 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 | 444 +++++++++++-----------
48 nixos/tests/sanoid.nix | 78 ++--
49 2 files changed, 265 insertions(+), 257 deletions(-)
50
51 diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
52 index 8b4c59155f..a011b9b24f 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 @@ -170,11 +83,15 @@ in
196 };
197
198 localTargetAllow = lib.mkOption {
199 - type = lib.types.listOf lib.types.str;
200 + type = with lib.types; listOf str;
201 + # Permission destroy is required to reset broken receive states (zfs receive -A),
202 + # which syncoid does when it fails to resume a receive state,
203 + # when the snapshot it refers to has been destroyed on the source.
204 default = [
205 "change-key"
206 "compression"
207 "create"
208 + "destroy"
209 "mount"
210 "mountpoint"
211 "receive"
212 @@ -198,7 +115,7 @@ in
213 };
214
215 commonArgs = lib.mkOption {
216 - type = lib.types.listOf lib.types.str;
217 + type = with lib.types; listOf str;
218 default = [ ];
219 example = [ "--no-sync-snap" ];
220 description = ''
221 @@ -296,13 +213,13 @@ in
222 '';
223 };
224
225 - useCommonArgs = lib.mkOption {
226 - type = lib.types.bool;
227 - default = true;
228 - description = ''
229 - Whether to add the configured common arguments to this command.
230 - '';
231 - };
232 + useCommonArgs =
233 + lib.mkEnableOption ''
234 + configured common arguments to this command
235 + ''
236 + // {
237 + default = true;
238 + };
239
240 service = lib.mkOption {
241 type = lib.types.attrs;
242 @@ -341,139 +258,216 @@ in
243 # Implementation
244
245 config = lib.mkIf cfg.enable {
246 - users = {
247 - users = lib.mkIf (cfg.user == "syncoid") {
248 - syncoid = {
249 - group = cfg.group;
250 - isSystemUser = true;
251 - # For syncoid to be able to create /var/lib/syncoid/.ssh/
252 - # and to use custom ssh_config or known_hosts.
253 - home = "/var/lib/syncoid";
254 - createHome = false;
255 - };
256 - };
257 - groups = lib.mkIf (cfg.group == "syncoid") {
258 - syncoid = { };
259 - };
260 - };
261 -
262 systemd.services = lib.mapAttrs' (
263 name: c:
264 + let
265 + sshKeyCred = builtins.split ":" c.sshKey;
266 + in
267 lib.nameValuePair "syncoid-${escapeUnitName name}" (
268 lib.mkMerge [
269 {
270 description = "Syncoid ZFS synchronization from ${c.source} to ${c.target}";
271 after = [ "zfs.target" ];
272 startAt = cfg.interval;
273 - # syncoid may need zpool to get feature@extensible_dataset
274 - path = [ "/run/booted-system/sw/bin/" ];
275 - serviceConfig = {
276 - ExecStartPre =
277 - (map (buildAllowCommand c.localSourceAllow) (localDatasetName c.source))
278 - ++ (map (buildAllowCommand c.localTargetAllow) (localDatasetName c.target));
279 - ExecStopPost =
280 - (map (buildUnallowCommand c.localSourceAllow) (localDatasetName c.source))
281 - ++ (map (buildUnallowCommand c.localTargetAllow) (localDatasetName c.target));
282 - ExecStart = lib.escapeShellArgs (
283 - [ "${cfg.package}/bin/syncoid" ]
284 - ++ lib.optionals c.useCommonArgs cfg.commonArgs
285 - ++ lib.optional c.recursive "-r"
286 - ++ lib.optionals (c.sshKey != null) [
287 - "--sshkey"
288 - c.sshKey
289 - ]
290 - ++ c.extraArgs
291 - ++ [
292 - "--sendoptions"
293 - c.sendOptions
294 - "--recvoptions"
295 - c.recvOptions
296 - "--no-privilege-elevation"
297 - c.source
298 - c.target
299 - ]
300 - );
301 - User = cfg.user;
302 - Group = cfg.group;
303 - StateDirectory = [ "syncoid" ];
304 - StateDirectoryMode = "700";
305 - # Prevent SSH control sockets of different syncoid services from interfering
306 - PrivateTmp = true;
307 - # Permissive access to /proc because syncoid
308 - # calls ps(1) to detect ongoing `zfs receive`.
309 - ProcSubset = "all";
310 - ProtectProc = "default";
311 + # Here we explicitly use the booted system to guarantee the stable API needed by ZFS.
312 + # Moreover syncoid may need zpool to get feature@extensible_dataset.
313 + path = [ "/run/booted-system/sw" ];
314 + # Prevents missing snapshots during DST changes
315 + environment.TZ = "UTC";
316 + # A custom LD_LIBRARY_PATH is needed to access in `getent passwd`
317 + # the systemd's entry about the DynamicUser=,
318 + # so that ssh won't fail with: "No user exists for uid $UID".
319 + environment.LD_LIBRARY_PATH = config.system.nssModules.path;
320 + serviceConfig =
321 + {
322 + ExecStartPre =
323 + # Recursively remove any residual permissions
324 + # given on local+descendant datasets (source, target or target's parent)
325 + # to any currently unknown (hence unused) systemd dynamic users (UID/GID range 61184…65519),
326 + # which happens when a crash has occurred
327 + # during any previous run of a syncoid-*.service (not only this one).
328 + map
329 + (
330 + dataset:
331 + "+"
332 + + pkgs.writeShellScript "zfs-unallow-unused-dynamic-users" ''
333 + set -eu
334 + zfs allow "$1" |
335 + sed -ne 's/^\t\(user\|group\) (unknown: \([0-9]\+\)).*/\1 \2/p' |
336 + {
337 + declare -a uids
338 + while read -r role id; do
339 + if [ "$id" -ge 61184 ] && [ "$id" -le 65519 ]; then
340 + case "$role" in
341 + (user) uids+=("$id");;
342 + esac
343 + fi
344 + done
345 + zfs unallow -r -u "$(printf %s, "''${uids[@]}")" "$1"
346 + }
347 + ''
348 + + " "
349 + + lib.escapeShellArg dataset
350 + )
351 + (
352 + localDatasetName c.source
353 + ++ localDatasetName c.target
354 + ++ map builtins.dirOf (localDatasetName c.target)
355 + )
356 + ++
357 + # For a local source, allow the localSourceAllow ZFS permissions.
358 + map (
359 + dataset:
360 + "+/run/booted-system/sw/bin/zfs allow $USER "
361 + + lib.escapeShellArgs [
362 + (lib.concatStringsSep "," c.localSourceAllow)
363 + dataset
364 + ]
365 + ) (localDatasetName c.source)
366 + ++
367 + # For a local target, check if the dataset exists before delegating permissions,
368 + # and if it doesn't exist, delegate it to the parent dataset.
369 + # This should solve the case of provisioning new datasets.
370 + map (
371 + dataset:
372 + "+"
373 + + pkgs.writeShellScript "zfs-allow-target" ''
374 + dataset="$1"
375 + # Run a ZFS list on the dataset to check if it exists
376 + zfs list "$dataset" >/dev/null 2>/dev/null ||
377 + dataset="$(dirname "$dataset")"
378 + zfs allow "$USER" ${lib.escapeShellArg (lib.concatStringsSep "," c.localTargetAllow)} "$dataset"
379 + ''
380 + + " "
381 + + lib.escapeShellArg dataset
382 + ) (localDatasetName c.target);
383 + ExecStopPost =
384 + let
385 + zfsUnallow = dataset: "+/run/booted-system/sw/bin/zfs unallow $USER " + lib.escapeShellArg dataset;
386 + in
387 + map zfsUnallow (localDatasetName c.source)
388 + ++
389 + # For a local target, unallow both the dataset and its parent,
390 + # because at this stage we have no way of knowing if the allow command
391 + # did execute on the parent dataset or not in the ExecStartPre=.
392 + # We can't run the same if-then-else in the post hook
393 + # since the dataset should have been created at this point.
394 + lib.concatMap (dataset: [
395 + (zfsUnallow dataset)
396 + (zfsUnallow (builtins.dirOf dataset))
397 + ]) (localDatasetName c.target);
398 + ExecStart = lib.escapeShellArgs (
399 + [ "${cfg.package}/bin/syncoid" ]
400 + ++ lib.optionals c.useCommonArgs cfg.commonArgs
401 + ++ lib.optional c.recursive "--recursive"
402 + ++ lib.optionals (c.sshKey != null) [
403 + "--sshkey"
404 + "\${CREDENTIALS_DIRECTORY}/${if lib.length sshKeyCred > 1 then lib.head sshKeyCred else "sshKey"}"
405 + ]
406 + ++ c.extraArgs
407 + ++ [
408 + "--sendoptions"
409 + c.sendOptions
410 + "--recvoptions"
411 + c.recvOptions
412 + "--no-privilege-elevation"
413 + c.source
414 + c.target
415 + ]
416 + );
417 + DynamicUser = true;
418 + NFTSet = lib.optional config.networking.nftables.enable "user:inet:filter:nixos_syncoid_uids";
419 + # Prevent SSH control sockets of different syncoid services from interfering
420 + PrivateTmp = true;
421 + # Permissive access to /proc because syncoid
422 + # calls ps(1) to detect ongoing `zfs receive`.
423 + ProcSubset = "all";
424 + ProtectProc = "default";
425
426 - # The following options are only for optimizing:
427 - # systemd-analyze security | grep syncoid-'*'
428 - AmbientCapabilities = "";
429 - CapabilityBoundingSet = "";
430 - DeviceAllow = [ "/dev/zfs" ];
431 - LockPersonality = true;
432 - MemoryDenyWriteExecute = true;
433 - NoNewPrivileges = true;
434 - PrivateDevices = true;
435 - PrivateMounts = true;
436 - PrivateNetwork = lib.mkDefault false;
437 - PrivateUsers = false; # Enabling this breaks on zfs-2.2.0
438 - ProtectClock = true;
439 - ProtectControlGroups = true;
440 - ProtectHome = true;
441 - ProtectHostname = true;
442 - ProtectKernelLogs = true;
443 - ProtectKernelModules = true;
444 - ProtectKernelTunables = true;
445 - ProtectSystem = "strict";
446 - RemoveIPC = true;
447 - RestrictAddressFamilies = [
448 - "AF_UNIX"
449 - "AF_INET"
450 - "AF_INET6"
451 - ];
452 - RestrictNamespaces = true;
453 - RestrictRealtime = true;
454 - RestrictSUIDSGID = true;
455 - RootDirectory = "/run/syncoid/${escapeUnitName name}";
456 - RootDirectoryStartOnly = true;
457 - BindPaths = [ "/dev/zfs" ];
458 - BindReadOnlyPaths = [
459 - builtins.storeDir
460 - "/etc"
461 - "/run"
462 - "/bin/sh"
463 - ];
464 - # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
465 - InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
466 - MountAPIVFS = true;
467 - # Create RootDirectory= in the host's mount namespace.
468 - RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
469 - RuntimeDirectoryMode = "700";
470 - SystemCallFilter = [
471 - "@system-service"
472 - # Groups in @system-service which do not contain a syscall listed by:
473 - # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
474 - # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
475 - # 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 ' '
476 - "~@aio"
477 - "~@chown"
478 - "~@keyring"
479 - "~@memlock"
480 - "~@privileged"
481 - "~@resources"
482 - "~@setuid"
483 - "~@timer"
484 - ];
485 - SystemCallArchitectures = "native";
486 - # This is for BindPaths= and BindReadOnlyPaths=
487 - # to allow traversal of directories they create in RootDirectory=.
488 - UMask = "0066";
489 - };
490 + # The following options are only for optimizing:
491 + # systemd-analyze security | grep syncoid-'*'
492 + AmbientCapabilities = "";
493 + CapabilityBoundingSet = "";
494 + DeviceAllow = [ "/dev/zfs" ];
495 + LockPersonality = true;
496 + MemoryDenyWriteExecute = true;
497 + NoNewPrivileges = true;
498 + PrivateDevices = true;
499 + PrivateMounts = true;
500 + PrivateNetwork = lib.mkDefault false;
501 + PrivateUsers = false; # Enabling this breaks on zfs-2.2.0
502 + ProtectClock = true;
503 + ProtectControlGroups = true;
504 + ProtectHome = true;
505 + ProtectHostname = true;
506 + ProtectKernelLogs = true;
507 + ProtectKernelModules = true;
508 + ProtectKernelTunables = true;
509 + ProtectSystem = "strict";
510 + RemoveIPC = true;
511 + RestrictAddressFamilies = [
512 + "AF_UNIX"
513 + "AF_INET"
514 + "AF_INET6"
515 + ];
516 + RestrictNamespaces = true;
517 + RestrictRealtime = true;
518 + RestrictSUIDSGID = true;
519 + RootDirectory = "/run/syncoid/${escapeUnitName name}";
520 + BindPaths = [ "/dev/zfs" ];
521 + BindReadOnlyPaths = [
522 + builtins.storeDir
523 + "/etc"
524 + "/run"
525 + # Some programs hardcode /var/run
526 + # eg. /var/run/avahi-daemon/socket to resolve *.local mDNS domains
527 + "/var/run"
528 + "/bin/sh"
529 + ];
530 + # Avoid useless mounting of RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
531 + InaccessiblePaths = [ "-+/run/syncoid/${escapeUnitName name}" ];
532 + MountAPIVFS = true;
533 + # Create RootDirectory= in the host's mount namespace.
534 + RuntimeDirectory = [ "syncoid/${escapeUnitName name}" ];
535 + RuntimeDirectoryMode = "700";
536 + SystemCallFilter = [
537 + "@system-service"
538 + # Groups in @system-service which do not contain a syscall listed by:
539 + # perf stat -x, 2>perf.log -e 'syscalls:sys_enter_*' syncoid …
540 + # awk >perf.syscalls -F "," '$1 > 0 {sub("syscalls:sys_enter_","",$3); print $3}' perf.log
541 + # 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 ' '
542 + "~@aio"
543 + "~@chown"
544 + "~@keyring"
545 + "~@memlock"
546 + "~@privileged"
547 + "~@resources"
548 + "~@setuid"
549 + ];
550 + SystemCallArchitectures = "native";
551 + # This is for BindPaths= and BindReadOnlyPaths=
552 + # to allow traversal of directories they create in RootDirectory=.
553 + UMask = "0066";
554 + }
555 + // (
556 + if lib.length sshKeyCred > 1 then
557 + { LoadCredentialEncrypted = [ c.sshKey ]; }
558 + else
559 + { LoadCredential = [ "sshKey:${c.sshKey}" ]; }
560 + );
561 }
562 cfg.service
563 c.service
564 ]
565 )
566 ) cfg.commands;
567 +
568 + networking.nftables.ruleset = lib.mkBefore ''
569 + table inet filter {
570 + # A set containing the dynamic UIDs of the syncoid services currently active
571 + set nixos_syncoid_uids { typeof meta skuid; }
572 + }
573 + '';
574 };
575
576 meta.maintainers = with lib.maintainers; [
577 diff --git a/nixos/tests/sanoid.nix b/nixos/tests/sanoid.nix
578 index e42fd54dfd..eabe88b6be 100644
579 --- a/nixos/tests/sanoid.nix
580 +++ b/nixos/tests/sanoid.nix
581 @@ -50,16 +50,26 @@ in
582 enable = true;
583 sshKey = "/var/lib/syncoid/id_ecdsa";
584 commands = {
585 - # Sync snapshot taken by sanoid
586 - "pool/sanoid" = {
587 - target = "root@target:pool/sanoid";
588 + # Take snapshot and sync
589 + "pool/syncoid".target = "root@target:pool/syncoid";
590 +
591 + # Sync the same dataset to different targets
592 + "pool/sanoid1" = {
593 + source = "pool/sanoid";
594 + target = "root@target:pool/sanoid1";
595 + extraArgs = [
596 + "--no-sync-snap"
597 + "--create-bookmark"
598 + ];
599 + };
600 + "pool/sanoid2" = {
601 + source = "pool/sanoid";
602 + target = "root@target:pool/sanoid2";
603 extraArgs = [
604 "--no-sync-snap"
605 "--create-bookmark"
606 ];
607 };
608 - # Take snapshot and sync
609 - "pool/syncoid".target = "root@target:pool/syncoid";
610
611 # Test pool without parent (regression test for https://github.com/NixOS/nixpkgs/pull/180111)
612 "pool".target = "root@target:pool/full-pool";
613 @@ -94,6 +104,7 @@ in
614 "zfs create pool/syncoid",
615 "udevadm settle",
616 )
617 +
618 target.succeed(
619 "mkdir /mnt",
620 "parted --script /dev/vdb -- mklabel msdos mkpart primary 1024M -1s",
621 @@ -106,41 +117,44 @@ in
622 "mkdir -m 700 -p /var/lib/syncoid",
623 "cat '${snakeOilPrivateKey}' > /var/lib/syncoid/id_ecdsa",
624 "chmod 600 /var/lib/syncoid/id_ecdsa",
625 - "chown -R syncoid:syncoid /var/lib/syncoid/",
626 )
627
628 - assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set before snapshotting"
629 - assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set before snapshotting"
630 - assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set before snapshotting"
631 + with subtest("Take snapshots with sanoid"):
632 + source.succeed("touch /mnt/pool/sanoid/test.txt")
633 + source.succeed("touch /mnt/pool/compat/test.txt")
634 + source.systemctl("start --wait sanoid.service")
635
636 - # Take snapshot with sanoid
637 - source.succeed("touch /mnt/pool/sanoid/test.txt")
638 - source.succeed("touch /mnt/pool/compat/test.txt")
639 - source.systemctl("start --wait sanoid.service")
640 + # Add some unused dynamic users to the stateful allow list of ZFS datasets,
641 + # simulating a state where they remain after the system crashed,
642 + # to check they'll be correctly removed by the syncoid services.
643 + # Each syncoid service run from now may reuse at most one of them for itself.
644 + source.succeed(
645 + "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool",
646 + "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool/sanoid",
647 + "zfs allow -u $(printf %s, {61184..61200})65519 dedup pool/syncoid",
648 + )
649 +
650 + with subtest("sync snapshots"):
651 + target.wait_for_open_port(22)
652 + source.succeed("touch /mnt/pool/syncoid/test.txt")
653 +
654 + source.systemctl("start --wait syncoid-pool-syncoid.service")
655 + target.succeed("cat /mnt/pool/syncoid/test.txt")
656 +
657 + source.systemctl("start --wait syncoid-pool-sanoid{1,2}.service")
658 + target.succeed("cat /mnt/pool/sanoid1/test.txt")
659 + target.succeed("cat /mnt/pool/sanoid2/test.txt")
660 +
661 + source.systemctl("start --wait syncoid-pool.service")
662 + target.succeed("[[ -d /mnt/pool/full-pool/syncoid ]]")
663 +
664 + source.systemctl("start --wait syncoid-pool-compat.service")
665 + target.succeed("cat /mnt/pool/compat/test.txt")
666
667 assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after snapshotting"
668 assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after snapshotting"
669 assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after snapshotting"
670 -
671 - # Sync snapshots
672 - target.wait_for_open_port(22)
673 - source.succeed("touch /mnt/pool/syncoid/test.txt")
674 - source.systemctl("start --wait syncoid-pool-sanoid.service")
675 - target.succeed("cat /mnt/pool/sanoid/test.txt")
676 - source.systemctl("start --wait syncoid-pool-syncoid.service")
677 - source.systemctl("start --wait syncoid-pool-syncoid.service")
678 - target.succeed("cat /mnt/pool/syncoid/test.txt")
679 -
680 assert(len(source.succeed("zfs list -H -t snapshot pool/syncoid").splitlines()) == 1), "Syncoid should only retain one sync snapshot"
681
682 - source.systemctl("start --wait syncoid-pool.service")
683 - target.succeed("[[ -d /mnt/pool/full-pool/syncoid ]]")
684 -
685 - source.systemctl("start --wait syncoid-pool-compat.service")
686 - target.succeed("cat /mnt/pool/compat/test.txt")
687 -
688 - assert len(source.succeed("zfs allow pool")) == 0, "Pool shouldn't have delegated permissions set after syncing snapshots"
689 - assert len(source.succeed("zfs allow pool/sanoid")) == 0, "Sanoid dataset shouldn't have delegated permissions set after syncing snapshots"
690 - assert len(source.succeed("zfs allow pool/syncoid")) == 0, "Syncoid dataset shouldn't have delegated permissions set after syncing snapshots"
691 '';
692 }
693 --
694 2.49.0
695