Zfs rollback not working using boot.initrd.systemd

Nix complains during building that I should change my working method to systemd. But I can’t get it to work.
I’m using disko

error:
       Failed assertions:
       - systemd stage 1 does not support 'boot.initrd.postDeviceCommands'. Please
         convert it to analogous systemd units in 'boot.initrd.systemd'.

This worked before but gives the error above:

      boot.initrd.postDeviceCommands = lib.mkAfter ''
        zfs rollback -r rpool/nixos/local/root@blank
      '';

This doesn’t rollback the system after boot:

      boot.initrd.systemd.enable = lib.mkDefault true;
      boot.initrd.systemd.services.rollback = {
        description = "Rollback ZFS datasets to a pristine state";
        wantedBy = [
          "initrd.target"
        ];
        after = [
          "zfs-import-zroot.service"
        ];
        before = [
          "sysroot.mount"
        ];
        path = with pkgs; [
          zfs
        ];
        unitConfig.DefaultDependencies = "no";
        serviceConfig.Type = "oneshot";
        script = ''
          zfs rollback -r rpool/nixos/local/root@blank && echo "rollback complete"
        '';
      };

other parts of the config:

      boot.loader.systemd-boot.enable = false;
      boot.loader.grub.enable = true;
      boot.loader.grub.efiSupport = true;
      boot.loader.efi.canTouchEfiVariables = true;

      boot.kernelPackages = config.boot.zfs.package.latestCompatibleLinuxPackages;

      boot.initrd.supportedFilesystems = [ "zfs" ]; # boot from zfs
      boot.kernelParams = [
        "elevator=none" # Because ZFS doesn't have the whole disk, just a partition. Set the disk’s scheduler to none. ZFS takes this step automatically if it does control the entire disk.
        "nohibernate" # ZFS misses support for freeze/thaw operations.This means that using ZFS together with hibernation (suspend to disk) may cause filesystem corruption.See https://github.com/openzfs/zfs/issues/260.
        # "boot.debug1devices"
      ];
      boot.supportedFilesystems = [ "zfs" ];
      boot.zfs.enableUnstable = false;
      # boot.zfs.forceImportRoot = false;
      # boot.zfs.forceImportAll = false;
      # boot.zfs.devNodes = "/dev/disk/by-partlabel/System";

      services.udev.extraRules = ''
        ACTION=="add|change", KERNEL=="sd[a-z]*[0-9]*|mmcblk[0-9]*p[0-9]*|nvme[0-9]*n[0-9]*p[0-9]*", ENV{ID_FS_TYPE}=="zfs_member", ATTR{../queue/scheduler}="none"
      ''; # zfs already has its own scheduler. without this (@Artturin)'s computer froze for a second when he nix builds something.

      services.zfs = {
        autoScrub.enable = true;
        trim.enable = true;
        autoSnapshot = {
          enable = lib.mkDefault true;
          frequent = 8; # 15min 
          hourly = 24; # 60min
          daily = 3; # 1d
          weekly = 4; # 1w
          monthly = 6; # 1m
          flags = "-k -p --utc";
        };
      };

      # TODO: Disko doesn't (yet) support marking filesystems as needed for boot.
      fileSystems = {
        "/".neededForBoot = true;
        # "/nix".neededForBoot = true;
        # "/cache".neededForBoot = true;
        # "/home".neededForBoot = true;
        "/persist".neededForBoot = true;
      };

disko:

{ disks
, swapInGB
, luks
, zfs_ashift
, zfs_refreservationInGB
, lib
, ...
}:
let
  single = (lib.length disks == 1);
in
{
  disko.devices = {
    disk = lib.genAttrs disks
      (device:
        let
          # NOTE: disk name should not be too long: https://github.com/nix-community/disko/issues/389
          # /dev/disk/by-id/ata-xxxxxx131403908
          n1 = lib.removePrefix "_" (builtins.replaceStrings [ "/" ] [ "_" ] (builtins.baseNameOf device));
          n2 = (lib.concatStrings [ "abcdefg" n1 ]); # Make it long enough if someone would use /dev/sda instead of by-id
          stringLength = builtins.stringLength n2;
          idLength = 6;
          idex1 = builtins.sub stringLength idLength;
          n3 = builtins.substring idex1 stringLength n2; # 403908
          n4 = (lib.concatStrings [ "main" n3 ]); # main403908
          dindex = lib.lists.findFirstIndex (d: d == device) null disks;
        in
        {
          inherit device;
          type = "disk";
          name = n4;
          content = {
            type = "gpt";
            partitions =
              {
                boot = {
                  size = "1M";
                  type = "EF02"; # for grub MBR
                };
                ESP = {
                  size = "1G";
                  type = "EF00";
                  content = {
                    type = "filesystem";
                    format = "vfat";
                  } // lib.optionalAttrs single {
                    mountpoint = "/boot";
                  } // lib.optionalAttrs (!single) {
                    mountpoint = "/boot${toString dindex}";
                    mountOptions = [ "nofail" ];
                  };
                };
              } // lib.optionalAttrs (swapInGB > 0) {
                SWAP = {
                  size = (lib.concatStrings [ (builtins.toString swapInGB) "G" ]);
                  content = {
                    type = "swap";
                    randomEncryption = true;
                    resumeDevice = true; # Resume from hiberation from this device
                  };
                };
              } // lib.optionalAttrs (!luks) {
                ZFS = {
                  size = "100%";
                  content = {
                    type = "zfs";
                    pool = "rpool";
                  };
                };
              } // lib.optionalAttrs luks {
                luks = {
                  size = "100%";
                  content = {
                    type = "luks";
                    name = "crypted";
                    # passwordFile strips the newline at the end
                    # keyFile is if the file is there for booting
                    # additionalKeyFiles are additional key files that can be also used for booting. But they don't need to be present
                    #   # disable settings.keyFile if you want to use interactive password entry
                    #   #passwordFile = "/tmp/secret.key"; # Interactive
                    #   settings = {
                    #     allowDiscards = true;
                    #     keyFile = "/tmp/secret.key";
                    #   };
                    #   additionalKeyFiles = [ "/tmp/additionalSecret.key" ];
                    content = {
                      type = "zfs";
                      pool = "rpool";
                    };
                  };
                };
              };
          };
        });
    zpool.rpool = {
      type = "zpool";
      mode = lib.mkIf (!single) "mirror";
      options = {
        ashift = (builtins.toString zfs_ashift);
        # Auto trimming could maybe be bad for my SSDs.  Will instead have the OS do `zpool trim` on a schedule.
        # autotrim = "on";
        listsnapshots = "on";
      };
      rootFsOptions = {
        # This is more or less required for certain things to not break, for systemd-journald posixacls are required
        acltype = "posixacl";
        canmount = "off";
        # zstd is slower but compresses more than lz4
        compression = "lz4";
        dnodesize = "auto";
        mountpoint = "none";
        normalization = "formD";
        atime = "on";
        relatime = "on";
        # To improve performance of certain extended attributes
        xattr = "sa";
        "com.sun:auto-snapshot" = "false";
      };
      postCreateHook = ''
        zfs snapshot -r rpool@blank
        # zfs set keylocation="prompt" "rpool";
      '';
      datasets = {
        # Static reservation so the pool will never be 100% full.
        #
        # If a pool fills up completely, delete this & reclaim space; don't
        # forget to re-create it afterwards!
        "reserved" = {
          type = "zfs_fs";
          options.canmount = "off";
          options.mountpoint = "none";
          options.refreservation = (lib.concatStrings [ (builtins.toString zfs_refreservationInGB) "G" ]);
          options.primarycache = "none";
          options.secondarycache = "none";
        };
        ## Root system container
        "nixos" = {
          type = "zfs_fs";
          options.mountpoint = "none";
        };
        ## Ephemeral datasets
        "nixos/local" = {
          type = "zfs_fs";
          options.mountpoint = "none";
        };
        "nixos/local/root" = {
          type = "zfs_fs";
          mountpoint = "/";
          options.mountpoint = "legacy";
          # options.mountpoint = "/";
        };
        "nixos/local/nix" = {
          type = "zfs_fs";
          mountpoint = "/nix";
          options.mountpoint = "legacy";
          # options.mountpoint = "/nix";
          # Disable writing access time, disables if a file's access time is updated when the file is read. This can result in significant performance gains, but might confuse some software like mailers.
          options.atime = "off";
          options.relatime = "off";
        };
        "nixos/local/cache" = {
          type = "zfs_fs";
          mountpoint = "/cache";
          options.mountpoint = "legacy";
          # options.mountpoint = "/cache";
        };
        ## Persistent datasets
        "nixos/safe" = {
          type = "zfs_fs";
          options.mountpoint = "none";
        };
        "nixos/safe/home" = {
          type = "zfs_fs";
          mountpoint = "/home";
          options.mountpoint = "legacy";
          # options.mountpoint = "/home";
          options."com.sun:auto-snapshot" = "true";
        };
        "nixos/safe/persist" = {
          type = "zfs_fs";
          mountpoint = "/persist";
          options.mountpoint = "legacy";
          # options.mountpoint = "/persist";
          options."com.sun:auto-snapshot" = "true";
        };
      };
    };
  };
}
1 Like

I am running a rollback if my rootfs on systems stage1 like this https://github.com/Shawn8901/nix-configuration/blob/af71d51998a6772a300f842795b947e27202fa73/machines/pointalpha/save-darlings.nix#L2 .
Maybe you can adapt it to your config.

1 Like

this works

      boot.initrd.systemd.enable = lib.mkDefault true;
      boot.initrd.systemd.services.rollback = {
        description = "Rollback root filesystem to a pristine state on boot";
        wantedBy = [
          # "zfs.target"
          "initrd.target"
        ];
        after = [
          "zfs-import-rpool.service"
        ];
        before = [
          "sysroot.mount"
        ];
        path = with pkgs; [
          zfs
        ];
        unitConfig.DefaultDependencies = "no";
        serviceConfig.Type = "oneshot";
        script = ''
          zfs rollback -r rpool/nixos/local/root@blank && echo "  >> >> rollback complete << <<"
        '';
      };

typo ‘zfs-import-rpool.service’ and not ‘zfs-import-zroot.service’

2 Likes