From 0350752a500567b0e932000a4a5484a01589dfc6 Mon Sep 17 00:00:00 2001 From: Madeleine Sydney Date: Thu, 12 Dec 2024 01:38:47 -0700 Subject: [PATCH] Use impermanence --- README.org | 12 +++ hosts/nixos-testbed/erase-home-darlings.clj | 84 --------------------- modules/home/impermanence.nix | 58 ++++++++++++++ modules/nixos/impermanence.nix | 66 ++++++++++++++++ 4 files changed, 136 insertions(+), 84 deletions(-) delete mode 100644 hosts/nixos-testbed/erase-home-darlings.clj create mode 100644 modules/home/impermanence.nix create mode 100644 modules/nixos/impermanence.nix diff --git a/README.org b/README.org index 4199daa..74e0cea 100644 --- a/README.org +++ b/README.org @@ -15,6 +15,8 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + impermanence.url = "github:nix-community/impermanence"; + home-manager = { url = "github:nix-community/home-manager"; inputs.nixpkgs.follows = "nixpkgs"; @@ -70,6 +72,7 @@ let mkHost = k: v: nixpkgs.lib.nixosSystem { inputs.self.nixosModules.default inputs.disko.nixosModules.disko + inputs.impermanence.nixosModules.impermanence # Directory name should always match host name. ({ ... }: { networking.hostName = k; }) @@ -102,6 +105,15 @@ builtins.mapAttrs mkHost (builtins.readDir ./hosts) ]; sydnix = { + impermanence = { + enable = true; + directories = [ + # Warning: Neither /var/lib/nixos nor any of its parents are persisted. + # This means all users/groups without specified uids/gids will have them + # reassigned on reboot. + "/var/lib/nixos" + ]; + }; users.users = [ "crumb" ]; diff --git a/hosts/nixos-testbed/erase-home-darlings.clj b/hosts/nixos-testbed/erase-home-darlings.clj deleted file mode 100644 index a146963..0000000 --- a/hosts/nixos-testbed/erase-home-darlings.clj +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bb - -;;; TODO: option to either move OR copy - -(require '[clojure.core.match :refer [match]] - '[babashka.cli :as cli] - '[babashka.process :refer [$ check] :as p]) - -(defn get-archive-path [] - (fs/path - "/persist/previous")) - -(defn get-files [] - (let [diff (:out (check ($ {:out :string} - "zfs" "diff" "-HF" - "rpool/local/home@blank" - "rpool/local/home")))] - ;; See zfs-diff(8) to understand what we're parsing here. - (->> diff - str/split-lines - (map #(str/split % #"\s")) - (filter #(and - ;; We only care to preserve /new/ content. - (contains? #{"+" "M"} (first %)) - ;; We only bother with plain old files. No directories, - ;; symlinks, etc. - (= (second %) "F"))) - (map #(nth % 2))))) - -(defn move-out-of-my-way [file] - ;; No TCO. }:< - (loop [n 0] - (let [file' (format "%s-%d" file n)] - (if (fs/exists? file') - (recur (inc n)) - (do (fs/move file file') - file'))))) - -(defn archive-files [archive-path files] - (let [new-archive (fs/path archive-path "home/new-archive")] - (when (fs/exists? new-archive) - (println "Warning: `new-archive' already exists... we'll rename it for you?") - (move-out-of-my-way new-archive)) - (doseq [file files] - (let [destination (fs/path - new-archive - (fs/relativize "/home" (fs/parent file)))] - (fs/create-dirs destination) - (fs/move file destination))))) - -(defn cycle-archives [archive-path n] - "Delete the oldest archive path, and increment each previous path by one. -More precisely, - - Delete the archive path labeled `n` (the oldest allowed). - - For each remaining path labeled 'i', relabel to 'i + 1'. - - Lastly, we delete the path labeled `new-archive`, if it exists." - (let [gp #(fs/path archive-path "home" (str %))] - (fs/delete-if-exists (gp n)) - (doseq [i (range (dec n) 1 -1)] - (when (fs/exists? (gp (dec i))) - (fs/move (gp (dec i)) (gp i)))) - (when (fs/exists? (gp "new-archive")) - (fs/move (gp "new-archive") (gp 1))))) - -(defn do-rollback [] - (let [proc (deref ($ "zfs" "rollback" "-r" "rpool/local/home@blank"))] - (if (= (:exit proc) 0) - (println (str "Successfully rolled back /home. " - "Enjoy the fresh filesystem smell! }:D")) - (println "Something went wrong rolling back /home... D:{")))) - -(defn -main [] - (let [n (if (< 0 (count *command-line-args*)) - (parse-long (first *command-line-args*)) - 3)] - (binding [p/*defaults* - {:pre-start-fn #(println (str "+ " (str/join (:cmd %))))}] - (let [archive-path (get-archive-path) - files (get-files)] - (archive-files archive-path files) - (cycle-archives archive-path n) - (do-rollback))))) - -(-main) diff --git a/modules/home/impermanence.nix b/modules/home/impermanence.nix new file mode 100644 index 0000000..65d9212 --- /dev/null +++ b/modules/home/impermanence.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.sydnix.impermanence; +in { + options = { + sydnix.impermanence = { + enable = mkOption { + description = "Enable Impermanence"; + type = types.bool; + default = false; + }; + + directories = mkOption { + description = ""; + type = with types; + listOf (coercedTo str (d: { directory = d; }) userDir); + default = []; + }; + + files = mkOption { + description = ""; + type = with types; + listOf (coercedTo str (f: { file = f; }) userFile); + default = []; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.erase-home-darlings = + let service = { + description = "Rollback home to a blank state on boot"; + wantedBy = [ + "multi-user.target" + ]; + after = [ + "home.mount" + ]; + path = [ pkgs.zfs pkgs.babashka ]; + # unitConfig.DefaultDependencies = "no"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = + let script = ./erase-home-darlings.clj; + in ''${pkgs.babashka}/bin/bb "${script}" 3''; + }; + stopIfChanged = false; + restartIfChanged = false; + }; + in if config.boot.initrd.systemd.enable + then service + else throw "sydnix.impermanence currently requires config.boot.initrd.systemd.enable'!"; + }; +} diff --git a/modules/nixos/impermanence.nix b/modules/nixos/impermanence.nix new file mode 100644 index 0000000..9d0cb58 --- /dev/null +++ b/modules/nixos/impermanence.nix @@ -0,0 +1,66 @@ +# Requires boot.initrd.enable = true and boot.initrd.systemd.enable = true! +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.sydnix.impermanence; +in { + options = { + sydnix.impermanence = { + enable = mkOption { + description = "Enable Impermanence"; + type = types.bool; + default = false; + }; + + directories = mkOption { + description = ""; + type = with types; listOf anything; + default = []; + }; + + files = mkOption { + description = ""; + + type = with types; listOf anything; + default = []; + }; + }; + }; + + config = mkIf cfg.enable { + boot.initrd.systemd.initrdBin = with pkgs; [ + zfs + ]; + + boot.initrd.systemd.services.erase-darlings = { + description = "Rollback filesystem to a blank state on boot"; + wantedBy = [ + "initrd.target" + ]; + after = [ + # "zfs-import.service" + "zfs-import-rpool.service" + ]; + before = [ + "sysroot.mount" + ]; + path = [ pkgs.zfs ]; + unitConfig.DefaultDependencies = "no"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = /* bash */ '' + zfs rollback -r rpool/local/root@blank \ + && echo ">> >> rollback complete << <<" + ''; + }; + + environment.persistence."/persist" = { + directories = cfg.directories; + files = cfg.files; + }; + }; +}