1
Files
flake-nixinator/system/modules/impermanence/default.nix

374 lines
14 KiB
Nix

{
config,
lib,
mylib,
username,
pkgs,
...
}: let
inherit (config.modules) impermanence;
in {
options.modules.impermanence = import ./options.nix {inherit lib mylib;};
config = let
# NOTE: Setting user/group/mode only has an effect if the
# directory is created by impermanence!
m777 = "u=rwx,g=rwx,o=rwx";
m755 = "u=rwx,g=rx,o=rx";
m711 = "u=rwx,g=x,o=x";
m700 = "u=rwx,g=,o=";
m644 = "u=rw,g=r,o=r";
m600 = "u=rw,g=,o=";
m444 = "u=r,g=r,o=r";
mkDir = user: directory: mode: {
inherit directory mode;
user = config.users.users.${user}.name;
group = config.users.users.${user}.group;
};
mkFile = user: file: mode: {
inherit file;
# This doesn't make much sense to set generally, e.g. when
# linking multiple files to ~/.config (they all have the same parent directory)
# parentDirectory = {
# inherit mode;
# user = config.users.users.${user}.name;
# group = config.users.users.${user}.group;
# };
};
mkRDir = mkDir "root";
mkRFile = mkFile "root";
mkUDir = mkDir "${username}";
mkUFile = mkFile "${username}";
# TODO: sth. like this, make options for configdirs/sharedirs/statedirs/homedirs
# populate options from respective modules, not here...
# mkConfigDirs = dirs:
# dirs
# |> builtins.map (dir: ".config/${dir}")
# |> builtins.map mkUDir # NOTE: mkUDir has wrong arg order
in
lib.mkIf impermanence.enable {
# TODO: Create options to allow host-specific impermanence setup
# inside the respective modules
environment.persistence."/persist" = {
hideMounts = false; # Sets x-gvfs-hide option
files = [
(mkRFile "/etc/adjtime" m644)
(mkRFile "/etc/machine-id" m444)
];
directories = [
(mkRDir "/etc/NetworkManager" m755)
(mkRDir "/etc/secureboot" m755)
(mkRDir "/etc/ssh" m755)
# https://github.com/nix-community/impermanence/issues/253
(mkRDir "/usr/systemd-placeholder" m755)
(mkRDir "/var/db/sudo" m711)
(mkRDir "/var/lib/bluetooth" m755) # m700
(mkRDir "/var/lib/containers" m755)
(mkRDir "/var/lib/flatpak" m755)
(mkRDir "/var/lib/libvirt" m755)
(mkRDir "/var/lib/NetworkManager" m755)
(mkRDir "/var/lib/nixos" m755)
(mkRDir "/var/lib/systemd" m755)
(mkRDir "/var/tmp" m777)
];
users.${username} = {
files = [
# NOTE: Don't put files generated/linked by HM here (they're already managed)
# TODO: Specifying files here (e.g. .config/QtProject.conf) doesn't seem to work
# They won't get mounted, also they can't be unmounted (because they're not mounted),
# which leads to /home not being unmounted correctly during shutdown...
];
directories = [
# Home directory
(mkUDir "Downloads" m755)
(mkUDir "Documents" m755)
(mkUDir "GitRepos" m755)
(mkUDir "NixFlake" m755)
(mkUDir "Notes" m755)
(mkUDir "Pictures" m755)
(mkUDir "Projects" m755)
(mkUDir "Public" m755)
(mkUDir "Unity" m755)
(mkUDir "Videos" m755)
# Secrets
(mkUDir ".gnupg" m755) # m600
(mkUDir ".secrets" m755) # m644
(mkUDir ".ssh" m755) # m644
# The shit some applications add to ~/ without asking
(mkUDir ".android" m755) # Unity
(mkUDir ".gradle" m755) # Unity
(mkUDir ".java" m755) # Unity/Rider
(mkUDir ".mozilla/firefox" m755) # TODO: Remove this someday
(mkUDir ".mozilla/native-messaging-hosts" m755)
(mkUDir ".nix-package-search" m755)
(mkUDir ".nv" m755) # Unity
(mkUDir ".ollama" m755)
(mkUDir ".plastic4" m755) # Unity
(mkUDir ".var/app" m755)
(mkUDir ".vim/undo" m755)
(mkUDir ".zotero" m755)
# Cache that's actually useful
(mkUDir ".cache/fish/generated_completions" m755)
(mkUDir ".cache/nix-index" m755)
(mkUDir ".cache/nix-search-tv" m755)
(mkUDir ".cache/nvim" m755)
# Config
(mkUDir ".config/.android" m755) # Unity
(mkUDir ".config/beets" m755)
(mkUDir ".config/blender" m755)
(mkUDir ".config/chromium" m755) # TODO: Remove this someday
(mkUDir ".config/Ferdium" m755)
(mkUDir ".config/fish/completions" m755)
(mkUDir ".config/impermanence" m755)
(mkUDir ".config/JetBrains" m755) # Unity/Rider
(mkUDir ".config/kdeconnect" m755)
(mkUDir ".config/keepassxc" m755)
(mkUDir ".config/Msty" m755)
(mkUDir ".config/Nextcloud" m755)
(mkUDir ".config/obsidian" m755)
(mkUDir ".config/obs-studio" m755)
(mkUDir ".config/Signal" m755)
(mkUDir ".config/singularitygroup-hotreload" m755) # Unity
(mkUDir ".config/tidal-hifi" m755)
(mkUDir ".config/tidal_dl_ng" m755)
(mkUDir ".config/unity3d" m755) # Unity
(mkUDir ".config/unityhub" m755) # Unity
(mkUDir ".config/vlc" m755)
(mkUDir ".config/Zeal" m755)
# Share
(mkUDir ".local/share/direnv" m755)
(mkUDir ".local/share/docker" m755)
(mkUDir ".local/share/fish" m755)
(mkUDir ".local/share/flatpak" m755)
(mkUDir ".local/share/JetBrains" m755) # Unity
(mkUDir ".local/share/hyprland" m755)
(mkUDir ".local/share/keyrings" m755) # m700
(mkUDir ".local/share/mime" m755)
(mkUDir ".local/share/nix" m755)
(mkUDir ".local/share/nvim" m755)
(mkUDir ".local/share/qutebrowser" m755)
(mkUDir ".local/share/systemd" m755)
(mkUDir ".local/share/unity3d" m755) # Unity
(mkUDir ".local/share/zoxide" m755)
# State
(mkUDir ".local/state/astal/notifd" m755)
(mkUDir ".local/state/home-manager/gc-roots" m755) # nix-flatpak stores its state there
(mkUDir ".local/state/lazygit" m755)
(mkUDir ".local/state/nix" m755)
(mkUDir ".local/state/nvim" m755)
(mkUDir ".local/state/wireplumber" m755)
];
};
};
# Add some helper scripts to identify files that might need persisting
environment.systemPackages = let
base = {
"root" = "/";
"home" = "/home/${username}";
};
ignore = {
"root" = "/home/${username}/.config/impermanence/fdignore-root";
"home" = "/home/${username}/.config/impermanence/fdignore-home";
};
move = {
"root" = "/persist/$(dirname {})";
"home" = "/persist/home/${username}/$(dirname {})";
};
mkHeader = "Press CTRL-R to reload, CTRL-M to move, CTRL-I to ignore file";
mkPreview = mode: "bat --color=always --theme=ansi --style=numbers --line-range=:100 ${base.${mode}}/{}";
mkReload = mode: "sudo fd --one-file-system --type f --hidden --base-directory ${base.${mode}} --ignore-file ${ignore.${mode}}";
mkIgnore = mode: "echo '{}' >> ${ignore.${mode}}";
mkMove = mode: "sudo mkdir -p ${move.${mode}} && sudo mv {} ${move.${mode}}";
mkScript = mode: ''
sudo ${mkReload mode} | \
sudo fzf \
--header "${mkHeader}" \
--preview "${mkPreview mode}" \
--bind "ctrl-r:reload:(${mkReload mode})" \
--bind "ctrl-i:execute:(${mkIgnore mode})" \
--bind "ctrl-m:execute:(${mkMove mode})"
'';
newroot = pkgs.writeShellScriptBin "newroot" (mkScript "root");
newhome = pkgs.writeShellScriptBin "newhome" (mkScript "home");
in [
newroot
newhome
];
# NOTE: This is REQUIRED for HM activation!
# Otherwise the home directory won't be writable!
systemd.services."impermanence-fix-home-ownership" = let
homeDir = "/home/${username}";
homeUser =
builtins.toString
config.users.users.${username}.uid;
homeGroup =
builtins.toString
config.users.groups.${config.users.users.${username}.group}.gid;
in {
description = "Fix impermanent home ownership";
wantedBy = ["home-manager-${username}.service"];
before = ["home-manager-${username}.service"];
after = ["home.mount"];
partOf = ["home.mount"];
serviceConfig.Type = "oneshot";
script = ''
# Don't chown if NFS shares are already mounted.
# This can happen outside of regular booting (e.g. nixos-rebuild switch),
# so we don't return an error.
# NOTE: Use || true as NixOS sets the damn -e, otherwise this unit fails on boot!
nfs_mounts=$(grep ' nfs4 ' /proc/mounts || true)
if [[ -n "$nfs_mounts" ]]; then
echo "NFS shares are mounted into the home directory, aborting:"
echo "$nfs_mounts"
exit 0
else
echo "No NFS shares are mounted into the home directory, continuing..."
fi
if [[ -d ${homeDir} ]]; then
chown -R ${homeUser}:${homeGroup} ${homeDir}
echo "Set ownership for ${homeDir} to ${homeUser}:${homeGroup}"
else
echo "ERROR: Home ${homeDir} does not exist!"
exit 1
fi
'';
};
# Because we have a LUKS encrypted drive
# we use a systemd service to cleanup the volumes
boot.initrd.systemd = {
enable = true;
services.impermanence-btrfs-cleanup = let
backupDuration = "7"; # Days
mountDir = "/btrfs_tmp";
persistDir = "${mountDir}/persist";
in {
description = "Clean impermanent btrfs subvolumes";
wantedBy = ["initrd.target"];
after = ["systemd-cryptsetup@crypted.service"];
before = ["sysroot.mount"];
unitConfig.DefaultDependencies = "no";
serviceConfig.Type = "oneshot";
# NOTE: If any single line of this script fails
# the entire system might be bricked.
# NixOS automatically sets "-e", so if unlucky,
# the subvolumes won'e exist for mounting...
script = let
mvSubvolToPersist = subvol: ''
if [[ -e ${mountDir}/${subvol} ]]; then
mkdir -p ${persistDir}/old_${subvol}s
timestamp=$(date --date="@$(stat -c %Y ${mountDir}/${subvol})" "+%Y-%m-%-d_%H:%M:%S")
mv ${mountDir}/${subvol} "${persistDir}/old_${subvol}s/$timestamp"
# Make the backup mutable (in case it is not, e.g. /var/empty)
# chattr -R -i -f "${persistDir}/old_${subvol}s/$timestamp"
echo "Backed up previous ${subvol} subvolume to ${persistDir}/old_${subvol}s/$timestamp"
fi
'';
mkNewSubvol = subvol: ''
if [[ ! -e ${mountDir}/${subvol} ]]; then
btrfs subvolume create ${mountDir}/${subvol}
echo "Created new subvolume ${mountDir}/${subvol}"
else
echo "Failed to move ${mountDir}/${subvol} (${mountDir}/${subvol} still exists), not creating new subvolume..."
fi
'';
# TODO: This fails and bricks the system
deleteOldBackups = subvol: ''
for old_${subvol} in $(find ${persistDir}/old_${subvol}s/ -maxdepth 1 -mtime +${backupDuration}); do
delete_subvolume_recursively "$old_${subvol}"
done
'';
in ''
# This dir will be created in the initrd ramdisk
mkdir -p ${mountDir}
# We mount the root subvolume. Because we're using a flat btrfs layout,
# "/" contains the subfolders (-volumes) home, log, nix, persist, root, swap, ...
mount -o subvol=/ /dev/mapper/crypted ${mountDir}
# Check if the persist dir exists so we can move stuff to it
if [[ ! -e ${persistDir} ]]; then
echo "${persistDir} doesn't exist, aborting..."
umount ${mountDir}
rmdir ${mountDir}
exit 0
fi
# Move root subvolume to backup location
${mvSubvolToPersist "root"}
# Move home subvolume to backup location
${mvSubvolToPersist "home"}
# Create new root subvolume
${mkNewSubvol "root"}
# Create new home subvolume
${mkNewSubvol "home"}
# TODO: Did this removal of old backups always brick the system?
# Delete a backed up subvolume
# delete_subvolume_recursively() {
# IFS=$'\n'
# # https://github.com/nix-community/impermanence/issues/258#issuecomment-2733383737
# # If we accidentally end up with a file or directory under old_roots,
# # the code will enumerate all subvolumes under the main volume.
# # We don't want to remove everything under true main volume. Only
# # proceed if this path is a btrfs subvolume (inode=256).
# if [ $(stat -c %i "$1") -ne 256 ]; then return; fi
# for subvol in $(btrfs subvolume list -o "$1" | cut -f 9- -d ' '); do
# delete_subvolume_recursively "${persistDir}/$subvol"
# done
# btrfs subvolume delete "$1"
# echo "Deleted old subvolume $1"
# }
# Delete old root backups
# $ {deleteOldBackups "root"}
# Delete old home backups
# $ {deleteOldBackups "home"}
umount ${mountDir}
rmdir ${mountDir}
'';
};
};
};
}