Preservation

repository·main·Indexed 43 days ago

https://github.com/nix-community/preservation

Nix tooling for the declarative management of non-volatile system state on NixOS. It provides an 'interpreter-less' approach to state management by generating static configuration, such as systemd-tmpfiles and systemd mount units, rather than relying on activation scripts. Requires nixos-24.11 or newer.

Tokens
3.8K
Snippets
10
Records
16
Agent score
51%

What's inside Preservation

  1. Overview of Preservation

    main
    Preservation is a Nix tooling project designed to enable the declarative management of non-volatile system state. It is inspired by the impermanence project but is not intended to be a drop-in replacement. Its goal is to provide state management for NixOS systems without relying on interpreters to perform the heavy lifting, enabling 'interpreter-less' state management.
  2. What is Impermanence and how does it relate to Preservation?

    main
    Impermanence is an established solution for managing persistent state on NixOS systems. Preservation is inspired by and heavily influenced by Impermanence. While Preservation provides its own mechanisms, users looking for the original implementation or a comparison of approaches should refer to the Impermanence project.
  3. Choose a preservation method with `how`

    main

    You can specify how a file or directory is preserved using the how option:

    • bindmount (default): Uses a bind mount to map the persistent path to the volatile path.
    • symlink: Creates a symbolic link on the volatile volume pointing to the persistent volume. This is useful for files that are expected to be initialized by a service (e.g., /var/lib/systemd/random-seed) and might not exist before the service runs.
    preservation.preserveAt."/persistent".files = [
      {
        file = "/var/lib/systemd/random-seed";
        how = "symlink";
        inInitrd = true;
      }
    ];
  4. Understand the differences between Preservation and impermanence

    main

    If you are deciding between Preservation and the impermanence module, consider these key architectural differences:

    • Scope: Preservation is specialized for NixOS and does not support non-NixOS systems via home-manager.
    • Mechanism: Preservation generates static configuration (specifically systemd-tmpfiles and systemd mount units). In contrast, impermanence uses NixOS activation scripts and custom bash-based systemd services to manage files, mounts, permissions, and ownership.
    • Runtime Logic: Preservation has no special runtime logic. Users are responsible for explicitly defining the setup timing, the method (symlink vs. bindmount), and parent directory permissions.
    • Mount Options: Preservation does not have a specific hideMounts option. Instead, it allows for arbitrary mount options to be applied to all bind-mounts via commonMountOptions.
  5. Configure permissions for intermediate path components

    main

    When a preserved path contains parent directories that do not exist on the volatile volume, Preservation handles them differently based on the scope:

    User-specific files

    For paths within a user's home directory (up to but not including the home directory itself), intermediate components are created with the respective user as owner and mode 0755.

    System-wide files

    For system-wide paths, missing intermediate components are created by systemd-tmpfiles with default ownership root:root and mode 0755.

    To configure custom ownership or modes for these intermediate directories, use systemd.tmpfiles.settings.preservation instead of relying on Preservation's configureParent option, as the latter can become convoluted for deep directory trees.

    # Example: Preserving /foo/bar/baz while configuring parents via tmpfiles
    
    preservation.preserveAt."/persistent".files = [
      { file = "/foo/bar/baz"; user = "foo"; group = "bar"; };
    ];
    
    systemd.tmpfiles.settings.preservation = {
      "/foo".d = { user = "foo"; group = "bar"; mode = "0775"; };
      "/foo/bar".d = { user = "bar"; group = "bar"; mode = "0755"; };
    };
  6. Configure Preservation setup timing and method

    main

    Because Preservation relies on static configuration rather than runtime scripts, you must explicitly define how and when files are preserved. When configuring Preservation, you must specify:

    1. Setup Timing: Whether the preservation should be set up in the initrd or after (the default).
    2. Setup Method: Whether to use symlink or bindmount (the default).
    3. Permissions: Whether parent directories of the persisted files require special permissions.
  7. Configure intermediate path components using `systemd-tmpfiles`

    main

    If you need to create or configure intermediate path components (parent directories) for a preserved system-wide path with custom permissions, use systemd.tmpfiles.settings.preservation. This is the recommended way to manage ownership and modes for directories that are not themselves the primary target of preservation.

    systemd.tmpfiles.settings.preservation = {
      "/foo".d = { user = "foo"; group = "bar"; mode = "0775"; };
      "/foo/bar".d = { user = "bar"; group = "bar"; mode = "0755"; };
    };
  8. Configure complex preservation for system and users

    main

    Preservation allows for granular control over system files, directories, and user-specific state.

    System Preservation

    • Directories: Can be specified as strings or as an attribute set. Use inInitrd = true for directories that must be available during the boot process (e.g., /var/lib/nixos).
    • Files: Use how = "symlink" to create symlinks on the volatile root pointing to the persistent volume. Use configureParent = true to ensure the directory structure exists on the persistent volume.

    User Preservation

    Define a users attribute set within preserveAt.<path>. Each user entry can specify:

    • home: The user's home directory (defaults to /home/${user}).
    • commonMountOptions: Mount options for the persistent volume.
    • directories: A list of directories to preserve. You can specify mode (e.g., "0700") for specific directories.
    • files: A list of files to preserve.

    Managing Permissions with systemd-tmpfiles

    When preserving user state, if a parent directory (like .local) is not an immediate parent of a preserved item, it may be created with root:root ownership. Use systemd.tmpfiles.settings to ensure these intermediate directories have the correct ownership and permissions so the user can access them.

    # configuration.nix
    {
      config,
      lib,
      pkgs,
      ...
    }:
    {
      preservation = {
        enable = true;
        preserveAt."/persistent" = {
          directories = [
            "/etc/secureboot"
            { directory = "/var/lib/nixos"; inInitrd = true; }
          ];
    
          files = [
            { file = "/etc/ssh/ssh_host_rsa_key"; how = "symlink"; configureParent = true; }
            "/var/lib/usbguard/rules.conf"
            { file = "/var/lib/systemd/random-seed"; how = "symlink"; inInitrd = true; configureParent = true; }
          ];
    
          users = {
            butz = {
              commonMountOptions = [ "x-gvfs-hide" ];
              directories = [
                { directory = ".ssh"; mode = "0700"; }
                ".config/syncthing"
              ];
              files = [ ".histfile" ];
            };
            root = {
              home = "/root";
              directories = [ { directory = ".ssh"; mode = "0700"; } ];
            };
          };
        };
      };
    
      systemd.tmpfiles.settings.preservation = {
        "/home/butz/.config".d = { user = "butz"; group = "users"; mode = "0755"; };
        "/home/butz/.local".d = { user = "butz"; group = "users"; mode = "0755"; };
      };
    }
  9. Preserve machine-id using symlinks and systemd compatibility

    main

    For setups requiring compatibility with systemd's ConditionFirstBoot, you can preserve the machine-id via a symlink instead of a bind-mount.

    Key configuration details:

    • Use how = "symlink" for the file entry.
    • Set configureParent = true to ensure the parent directory (e.g., /persistent/etc/) is created on the persistent volume.
    • To ensure the transient ID is correctly committed to the persistent volume, override systemd.services.systemd-machine-id-commit with a ConditionPathIsMountPoint check and a systemd-machine-id-setup --commit command targeting the persistent root.
    # configuration.nix
    {
      config,
      lib,
      pkgs,
      ...
    }:
    {
      preservation = {
        enable = true;
        preserveAt."/persistent" = {
          files = [
            # auto-generated machine ID
            { file = "/etc/machine-id"; inInitrd = true; how = "symlink"; configureParent = true; }
          ];
          directories = [ "/" ];
        };
      };
    
      systemd.suppressedSystemUnits = [ "systemd-machine-id-commit.service" ];
    
      # let the service commit the transient ID to the persistent volume
      systemd.services.systemd-machine-id-commit = {
        unitConfig.ConditionPathIsMountPoint = [ "" "/persistent/etc/machine-id" ];
        serviceConfig.ExecStart = [ "" "systemd-machine-id-setup --commit --root /persistent" ];
      };
    }
  10. Use the Preservation library in Nix modules

    main

    The core logic for discovering persisted files/directories and generating tmpfiles configurations and mount units is exposed via lib.nix.

    To use this functionality in your own Nix modules, you must instantiate the Preservation library by passing the standard nixpkgs.lib to it. You can access this library either by importing lib.nix directly or by using the lib output from the Preservation flake.

    # Example: Instantiating the library with nixpkgs lib
    let
      nixpkgs = import <nixpkgs> {};
      preservationLib = import ./lib.nix { lib = nixpkgs.lib; };
    in
    # Use preservationLib for file discovery and config generation
    { ... }