Commit 67499e2

HPCesia <me@hpcesia.com>
2026-06-10 04:53:42
Add Impermanence
1 parent f868dce
modules/core/impermanence/btrfs.nix
@@ -0,0 +1,94 @@
+{
+  den,
+  lib,
+  ...
+}: {
+  den.aspects.core.impermanence.btrfs = {
+    settings = {
+      partId = lib.mkOption {
+        type = lib.types.str;
+        default = "";
+        description = ''
+          Disk part id (e.g., "/dev/disk/by-partuuid/...").
+          If not set, auto-detect when aspects.disk.disko is included.
+        '';
+      };
+      preserveTime = lib.mkOption {
+        type = lib.types.ints.positive;
+        default = 30;
+        description = ''
+          Time old root/home subvolumes preserved.
+        '';
+      };
+    };
+
+    nixos = {host, ...}: let
+      cfg = host.settings.core.impermanence.btrfs;
+
+      partDevice =
+        if cfg.partId != ""
+        then
+          if lib.hasPrefix "/dev/" cfg.partId
+          then cfg.partId
+          else "/dev/disk/by-partuuid/" + cfg.partId
+        else if (host.hasAspect den.aspects.disk.disko)
+        then "/dev/disk/by-partlabel/disk-main-nixos"
+        else abort "Host not included disk.disko aspect, Please set settings.core.impermanence.btrfs.partId.";
+
+      partDeviceUnit =
+        lib.removePrefix "-" (
+          lib.replaceStrings ["-" "/"] ["\\x2d" "-"] partDevice
+        )
+        + ".device";
+
+      script = subvol: ''
+        mkdir -p /mnt
+
+        mount ${partDevice} /mnt
+
+        mkdir -p /mnt/${subvol}_old
+        timestamp=$(date "+%Y-%m-%-d_%H:%M:%S")
+        rm -rf "/mnt/${subvol}_old/$timestamp"
+        mv /mnt/${subvol} /mnt/${subvol}_old/$timestamp
+
+        delete_subvolume_recursively() {
+            IFS=$'\n'
+            for i in $(btrfs subvolume list -o "$1" | cut -f 9- -d ' '); do
+                delete_subvolume_recursively "/mnt/$i"
+            done
+            btrfs subvolume delete "$1"
+        }
+
+        for i in $(find /mnt/${subvol}_old -maxdepth 1 -mtime +${toString cfg.preserveTime}); do
+            delete_subvolume_recursively "$i"
+        done
+
+        btrfs subvolume create /mnt/${subvol}
+        umount /mnt
+      '';
+    in {
+      boot.initrd.systemd.services = lib.mkIf (host.hasAspect den.aspects.disk.btrfs) {
+        rollback-btrfs-root = {
+          description = "Rollback and clean old btrfs root subvolumes";
+          wantedBy = ["initrd.target"];
+          before = ["sysroot.mount"];
+          after = [partDeviceUnit];
+          requires = [partDeviceUnit];
+          unitConfig.DefaultDependencies = false;
+          serviceConfig.Type = "oneshot";
+          script = script "root";
+        };
+        rollback-btrfs-home = {
+          description = "Rollback and clean old btrfs home subvolumes";
+          wantedBy = ["initrd.target"];
+          before = ["home.mount"];
+          after = [partDeviceUnit];
+          requires = [partDeviceUnit];
+          unitConfig.DefaultDependencies = false;
+          serviceConfig.Type = "oneshot";
+          script = script "home";
+        };
+      };
+    };
+  };
+}
modules/core/impermanence/default.nix
@@ -0,0 +1,54 @@
+{
+  den,
+  inputs,
+  ...
+}: {
+  flake-file.inputs.impermanence = {
+    url = "github:nix-community/impermanence";
+    inputs.nixpkgs.follows = "nixpkgs";
+    inputs.home-manager.follows = "home-manager";
+  };
+
+  den.aspects.core.impermanence = {
+    includes = with den.aspects.core.impermanence; [
+      persist-collector
+      persist-user-collector
+      persist-home-collector
+
+      btrfs
+    ];
+
+    persist = {
+      directories = [
+        "/etc/NetworkManager/system-connections"
+      ];
+      files = [
+        "/root/.bash_history"
+      ];
+    };
+    cache = {
+      directories = [
+        "/var/lib/nixos"
+        "/srv"
+      ];
+    };
+    persistUser = {
+      files = [
+        ".bash_history"
+      ];
+    };
+
+    nixos = {lib, ...}: {
+      imports = [inputs.impermanence.nixosModules.impermanence];
+
+      environment.persistence."/persist" = {
+        enable = true;
+        hideMounts = lib.mkDefault true;
+      };
+      environment.persistence."/cache" = {
+        enable = true;
+        hideMounts = lib.mkDefault true;
+      };
+    };
+  };
+}
modules/core/impermanence/quirks.nix
@@ -0,0 +1,81 @@
+{
+  den,
+  lib,
+  ...
+}: let
+  # Shared collector body for the home-manager persistence quirks. Reused by
+  # two distinct aspects so that the same logic runs at both the host scope
+  # (projected onto users via the host-aspects battery) and the user scope.
+  homeCollector = {
+    persistHome,
+    cacheHome,
+    config,
+    ...
+  }: let
+    _removeHomePrefix = path: lib.removePrefix "${config.home.homeDirectory}/" path;
+    removeHomePrefix = path:
+      if (lib.typeOf path == "string")
+      then _removeHomePrefix path
+      else path // {directory = _removeHomePrefix path.directory;};
+    removeHomePrefixes = paths: lib.map removeHomePrefix paths;
+    mergePersist = entries: {
+      directories = lib.unique (lib.concatMap (e: removeHomePrefixes (e.directories or [])) entries);
+      files = lib.unique (lib.concatMap (e: removeHomePrefixes (e.files or [])) entries);
+    };
+  in {
+    home.persistence."/persist" = mergePersist persistHome;
+    home.persistence."/cache" = mergePersist cacheHome;
+  };
+in {
+  den.quirks.persist.description = "Persistent directories/files collected from aspects (host)";
+  den.quirks.cache.description = "Cache directories/files collected from aspects (host)";
+  den.quirks.persistUser.description = "Persistent directories/files collected from aspects (user)";
+  den.quirks.cacheUser.description = "Cache directories/files collected from aspects (user)";
+  den.quirks.persistHome.description = "Persistent directories/files collected from aspects (home manager)";
+  den.quirks.cacheHome.description = "Cache directories/files collected from aspects (home manager)";
+
+  den.aspects.core.impermanence.persist-collector = {
+    nixos = {
+      persist,
+      cache,
+      ...
+    }: let
+      mergePersist = entries: {
+        directories = lib.unique (lib.concatMap (e: e.directories or []) entries);
+        files = lib.unique (lib.concatMap (e: e.files or []) entries);
+      };
+    in {
+      environment.persistence."/persist" = mergePersist persist;
+      environment.persistence."/cache" = mergePersist cache;
+    };
+  };
+
+  den.aspects.core.impermanence.persist-user-collector = {user, ...}: {
+    nixos = {
+      persistUser,
+      cacheUser,
+      ...
+    }: let
+      mergePersist = entries: {
+        directories = lib.unique (lib.concatMap (e: e.directories or []) entries);
+        files = lib.unique (lib.concatMap (e: e.files or []) entries);
+      };
+    in {
+      environment.persistence."/persist".users.${user.userName} = mergePersist persistUser;
+      environment.persistence."/cache".users.${user.userName} = mergePersist cacheUser;
+    };
+  };
+
+  den.aspects.core.impermanence.persist-home-collector.homeManager = homeCollector;
+  den.aspects.core.impermanence.persist-home-user-collector.homeManager = homeCollector;
+
+  den.default.includes = (
+    den.lib.policy.when
+    ({host, ...}:
+      host.hasAspect {
+        name = "impermanence";
+        meta.provider = ["core"];
+      })
+    [den.aspects.core.impermanence.persist-home-user-collector]
+  );
+}
modules/core/openssh.nix
@@ -1,5 +1,20 @@
 {lib, ...}: {
   den.aspects.core.openssh = {
+    persist = {config, ...}: {
+      files =
+        builtins.concatMap
+        (key: [key.path] ++ ["${key.path}.pub"])
+        config.services.openssh.hostKeys;
+    };
+    persistUser = {
+      directories = [
+        {
+          directory = ".ssh";
+          mode = "0700";
+        }
+      ];
+    };
+
     nixos = {
       services.openssh = {
         generateHostKeys = true;
modules/core/systemd.nix
@@ -1,5 +1,28 @@
 {
   den.aspects.core.systemd = {
+    persist = {
+      files = [
+        "/etc/machine-id"
+      ];
+    };
+    cache = {
+      directories = [
+        "/var/lib/systemd/coredump"
+        "/var/lib/systemd/timers"
+        "/var/lib/systemd/catalog"
+        {
+          directory = "/var/lib/systemd/network";
+          mode = "0755";
+          user = "systemd-network";
+          group = "systemd-network";
+        }
+        "/var/log"
+      ];
+      files = [
+        "/var/lib/lastlog/lastlog2.db"
+      ];
+    };
+
     nixos = {
       systemd.tmpfiles.rules = [
         "d /var/lib/systemd/coredump 0755 root root 7d"
modules/disk/btrfs-disko.nix
@@ -110,11 +110,11 @@
                           mountpoint = "/nix";
                           mountOptions = defaultBtrfsOpts ++ ["noatime"];
                         };
-                        "/persist" = {
+                        "/persist" = lib.mkIf (host.hasAspect den.aspects.core.impermanence) {
                           mountpoint = "/persist";
                           mountOptions = defaultBtrfsOpts;
                         };
-                        "/cache" = {
+                        "/cache" = lib.mkIf (host.hasAspect den.aspects.core.impermanence) {
                           mountpoint = "/cache";
                           mountOptions = defaultBtrfsOpts;
                         };
modules/hosts/tribios/file-system.nix
@@ -1,4 +1,8 @@
 {den, ...}: {
+  den.hosts.aarch64-linux.tribios = {
+    settings.core.impermanence.btrfs.partId = "/dev/disk/by-uuid/9886235f-5a74-41e8-919b-9e4096a7cb9e";
+  };
+
   den.aspects.tribios = {
     # Manually manage file system.
     # Since tribios has edk2-rk3588 firmware in main storage.
modules/roles/default.nix
@@ -13,6 +13,7 @@
       core.hardware.thunderbolt
 
       core.editor
+      core.impermanence
       core.localization
       core.nameservers
       core.nftables
flake.lock
@@ -123,6 +123,29 @@
         "type": "github"
       }
     },
+    "impermanence": {
+      "inputs": {
+        "home-manager": [
+          "home-manager"
+        ],
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1769548169,
+        "narHash": "sha256-03+JxvzmfwRu+5JafM0DLbxgHttOQZkUtDWBmeUkN8Y=",
+        "owner": "nix-community",
+        "repo": "impermanence",
+        "rev": "7b1d382faf603b6d264f58627330f9faa5cba149",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "impermanence",
+        "type": "github"
+      }
+    },
     "import-tree": {
       "locked": {
         "lastModified": 1778781969,
@@ -163,6 +186,7 @@
         "flake-file": "flake-file",
         "flake-parts": "flake-parts",
         "home-manager": "home-manager",
+        "impermanence": "impermanence",
         "import-tree": "import-tree",
         "nixpkgs": "nixpkgs"
       }
flake.nix
@@ -25,6 +25,13 @@
       url = "github:nix-community/home-manager";
       inputs.nixpkgs.follows = "nixpkgs";
     };
+    impermanence = {
+      url = "github:nix-community/impermanence";
+      inputs = {
+        home-manager.follows = "home-manager";
+        nixpkgs.follows = "nixpkgs";
+      };
+    };
     import-tree.url = "github:denful/import-tree";
     nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
   };