impermanence/nixos.nix
talyz 8fc761e8c3
nixos: Only check file systems for neededForBoot
Only assert that persistent paths are marked neededForBoot if they're
file system roots.

Fixes #12
2020-07-18 21:51:16 +02:00

211 lines
7.7 KiB
Nix

{ pkgs, config, lib, ... }:
with lib;
let
cfg = config.environment.persistence;
persistentStoragePaths = attrNames cfg;
inherit (pkgs.callPackage ./lib.nix { }) splitPath dirListToPath concatPaths;
in
{
options = {
environment.persistence = mkOption {
default = { };
type = with types; attrsOf (
submodule {
options =
{
files = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
Files in /etc that should be stored in persistent storage.
'';
};
directories = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
Directories to bind mount to persistent storage.
'';
};
};
}
);
};
};
config = {
environment.etc =
let
link = file:
pkgs.runCommand
"${replaceStrings [ "/" "." " " ] [ "-" "" "" ] file}"
{ }
"ln -s '${file}' $out";
# Create environment.etc link entry.
mkLinkNameValuePair = persistentStoragePath: file: {
name = removePrefix "/etc/" file;
value = { source = link (concatPaths [ persistentStoragePath file ]); };
};
# Create all environment.etc link entries for a specific
# persistent storage path.
mkLinksToPersistentStorage = persistentStoragePath:
listToAttrs (map
(mkLinkNameValuePair persistentStoragePath)
cfg.${persistentStoragePath}.files
);
in
foldl' recursiveUpdate { } (map mkLinksToPersistentStorage persistentStoragePaths);
fileSystems =
let
# Create fileSystems bind mount entry.
mkBindMountNameValuePair = persistentStoragePath: dir: {
name = concatPaths [ "/" dir ];
value = {
device = concatPaths [ persistentStoragePath dir ];
noCheck = true;
options = [ "bind" ];
};
};
# Create all fileSystems bind mount entries for a specific
# persistent storage path.
mkBindMountsForPath = persistentStoragePath:
listToAttrs (map
(mkBindMountNameValuePair persistentStoragePath)
cfg.${persistentStoragePath}.directories
);
in
foldl' recursiveUpdate { } (map mkBindMountsForPath persistentStoragePaths);
system.activationScripts =
let
# Create a directory in persistent storage, so we can bind
# mount it. The directory structure's mode and ownership mirror those of
# persistentStoragePath/dir;
# TODO: Move this script to it's own file, add CI with shfmt/shellcheck.
mkDirWithPerms = persistentStoragePath: dir:
''
# Given a source directory, /source, and a target directory,
# /target/foo/bar/bazz, we want to "clone" the target structure
# from source into the target. Essentially, we want both
# /source/target/foo/bar/bazz and /target/foo/bar/bazz to exist
# on the filesystem. More concretely, we'd like to map
# /state/etc/ssh/example.key to /etc/ssh/example.key
#
# To achieve this, we split the target's path into parts -- target, foo,
# bar, bazz -- and iterate over them while accumulating the path
# (/target/, /target/foo/, /target/foo/bar, and so on); then, for each of
# these increasingly qualified paths we:
#
# 1. Ensure both /source/qualifiedPath and qualifiedPath exist
# 2. Copy the ownership of the source path into the target path
# 3. Copy the mode of the source path into the target path
(
# capture the nix vars into bash to avoid escape hell
sourceBase="${persistentStoragePath}"
target="${dir}"
# trim trailing slashes the root of all evil
sourceBase="''${sourceBase%/}"
target="''${target%/}"
# check that the source exists, if it doesn't exit the underlying
# scope (the activation script will continue)
realSource="$(realpath "$sourceBase$target")"
if [ ! -d "$realSource" ]; then
printf "\e[1;31mBind source '%s' does not exist; it will be created for you.\e[0m\n" "$realSource"
fi
# iterate over each part of the target path, e.g. var, lib, iwd
previousPath="/"
for pathPart in $(echo "$target" | tr "/" " "); do
# construct the incremental path, e.g. /var, /var/lib, /var/lib/iwd
currentTargetPath="$previousPath$pathPart/"
# construct the source path, e.g. /state/var, /state/var/lib, ...
currentSourcePath="$sourceBase$currentTargetPath"
# create the source and target directories if they don't exist
[ -d "$currentSourcePath" ] || mkdir "$currentSourcePath"
[ -d "$currentTargetPath" ] || mkdir "$currentTargetPath"
# resolve the source path to avoid symlinks
currentRealSourcePath="$(realpath "$currentSourcePath")"
# synchronize perms between source and target
chown --reference="$currentRealSourcePath" "$currentTargetPath"
chmod --reference="$currentRealSourcePath" "$currentTargetPath"
# lastly we update the previousPath to continue down the tree
previousPath="$currentTargetPath"
done
)
'';
# Build an activation script which creates all persistent
# storage directories we want to bind mount.
mkDirCreationScriptForPath = persistentStoragePath:
nameValuePair
"createDirsIn-${replaceStrings [ "/" "." " " ] [ "-" "" "" ] persistentStoragePath}"
(noDepEntry (concatMapStrings
(mkDirWithPerms persistentStoragePath)
cfg.${persistentStoragePath}.directories
));
in
listToAttrs (map mkDirCreationScriptForPath persistentStoragePaths);
assertions =
let
files = concatMap (p: p.files or [ ]) (attrValues cfg);
markedNeededForBoot = cond: fs:
if config.fileSystems ? ${fs} then
(config.fileSystems.${fs}.neededForBoot == cond)
else
cond;
in
[
{
# Assert that files are put in /etc, a current limitation,
# since we're using environment.etc.
assertion = all (hasPrefix "/etc") files;
message =
let
offenders = filter (file: !(hasPrefix "/etc" file)) files;
in
''
environment.persistence.files:
Currently, only files in /etc are supported.
Please fix or remove the following paths:
${concatStringsSep "\n " offenders}
'';
}
{
# Assert that all persistent storage volumes we use are
# marked with neededForBoot.
assertion = all (markedNeededForBoot true) persistentStoragePaths;
message =
let
offenders = filter (markedNeededForBoot false) persistentStoragePaths;
in
''
environment.persistence:
All filesystems used for persistent storage must
have the flag neededForBoot set to true.
Please fix or remove the following paths:
${concatStringsSep "\n " offenders}
'';
}
];
};
}