impermanence/nixos.nix

613 lines
21 KiB
Nix
Raw Normal View History

2020-06-04 21:42:16 +00:00
{ pkgs, config, lib, ... }:
let
2022-11-13 17:16:57 +00:00
inherit (lib)
attrNames
attrValues
zipAttrsWith
flatten
mkOption
mkDefault
mapAttrsToList
types
foldl'
unique
concatMapStrings
listToAttrs
escapeShellArg
escapeShellArgs
recursiveUpdate
all
filter
filterAttrs
concatStringsSep
concatMapStringsSep
isString
catAttrs
optional
literalExpression
;
2022-11-13 17:16:57 +00:00
inherit (pkgs.callPackage ./lib.nix { })
splitPath
dirListToPath
concatPaths
sanitizeName
duplicates
;
2020-06-04 21:42:16 +00:00
cfg = config.environment.persistence;
users = config.users.users;
allPersistentStoragePaths = { directories = [ ]; files = [ ]; users = [ ]; }
// (zipAttrsWith (_name: flatten) (attrValues cfg));
inherit (allPersistentStoragePaths) files directories;
mountFile = pkgs.runCommand "impermanence-mount-file" { buildInputs = [ pkgs.bash ]; } ''
cp ${./mount-file.bash} $out
patchShebangs $out
'';
2022-02-11 01:12:23 +00:00
# Create fileSystems bind mount entry.
mkBindMountNameValuePair = { directory, persistentStoragePath, ... }: {
name = concatPaths [ "/" directory ];
value = {
device = concatPaths [ persistentStoragePath directory ];
noCheck = true;
options = [ "bind" ]
++ optional cfg.${persistentStoragePath}.hideMounts "x-gvfs-hide";
depends = [ persistentStoragePath ];
2022-02-11 01:12:23 +00:00
};
};
# Create all fileSystems bind mount entries for a specific
# persistent storage path.
bindMounts = listToAttrs (map mkBindMountNameValuePair directories);
2020-06-04 21:42:16 +00:00
in
{
options = {
environment.persistence = mkOption {
default = { };
type =
let
2022-11-13 17:16:57 +00:00
inherit (types)
attrsOf
bool
listOf
submodule
nullOr
path
either
str
coercedTo
;
in
attrsOf (
submodule (
{ name, config, ... }:
let
persistentStoragePath = name;
defaultPerms = {
mode = "0755";
user = "root";
group = "root";
};
commonOpts = {
options = {
persistentStoragePath = mkOption {
type = path;
default = persistentStoragePath;
description = ''
The path to persistent storage where the real
file should be stored.
'';
};
};
2020-06-04 21:42:16 +00:00
};
dirPermsOpts = {
user = mkOption {
type = str;
description = ''
If the directory doesn't exist in persistent
storage it will be created and owned by the user
specified by this option.
'';
};
group = mkOption {
type = str;
description = ''
If the directory doesn't exist in persistent
storage it will be created and owned by the
group specified by this option.
'';
};
mode = mkOption {
type = str;
example = "0700";
description = ''
If the directory doesn't exist in persistent
storage it will be created with the mode
specified by this option.
'';
};
};
fileOpts = {
options = {
file = mkOption {
type = str;
description = ''
The path to the file.
'';
};
parentDirectory = dirPermsOpts;
};
2020-06-04 21:42:16 +00:00
};
dirOpts = {
options = {
directory = mkOption {
type = str;
description = ''
The path to the directory.
'';
};
} // dirPermsOpts;
};
rootFile = submodule [
commonOpts
fileOpts
{ parentDirectory = mkDefault defaultPerms; }
];
rootDir = submodule ([
commonOpts
dirOpts
] ++ (mapAttrsToList (n: v: { ${n} = mkDefault v; }) defaultPerms));
in
{
options =
{
users = mkOption {
type = attrsOf (
submodule (
{ name, config, ... }:
let
userDefaultPerms = {
inherit (defaultPerms) mode;
user = name;
group = users.${userDefaultPerms.user}.group;
};
userFile = submodule [
commonOpts
fileOpts
{ parentDirectory = mkDefault userDefaultPerms; }
];
userDir = submodule ([
commonOpts
dirOpts
] ++ (mapAttrsToList (n: v: { ${n} = mkDefault v; }) userDefaultPerms));
in
{
options =
{
# Needed because defining fileSystems
# based on values from users.users
# results in infinite recursion.
home = mkOption {
type = path;
default = "/home/${userDefaultPerms.user}";
defaultText = "/home/<username>";
description = ''
The user's home directory. Only
useful for users with a custom home
directory path.
Cannot currently be automatically
deduced due to a limitation in
nixpkgs.
'';
};
files = mkOption {
type = listOf (either str userFile);
default = [ ];
example = [
".screenrc"
];
description = ''
Files that should be stored in
persistent storage.
'';
apply =
map (file:
if isString file then
{
inherit persistentStoragePath;
file = concatPaths [ config.home file ];
parentDirectory = userDefaultPerms;
}
else
file // {
file = concatPaths [ config.home file.file ];
});
};
directories = mkOption {
type = listOf (either str userDir);
default = [ ];
example = [
"Downloads"
"Music"
"Pictures"
"Documents"
"Videos"
];
description = ''
Directories to bind mount to
persistent storage.
'';
apply =
map (directory:
if isString directory then
userDefaultPerms // {
directory = concatPaths [ config.home directory ];
inherit persistentStoragePath;
}
else
directory // {
directory = concatPaths [ config.home directory.directory ];
});
};
};
}
)
);
default = { };
description = ''
A set of user submodules listing the files and
directories to link to their respective user's
home directories.
Each attribute name should be the name of the
user.
For detailed usage, check the <link
xlink:href="https://github.com/nix-community/impermanence">documentation</link>.
'';
example = literalExpression ''
{
talyz = {
directories = [
"Downloads"
"Music"
"Pictures"
"Documents"
"Videos"
"VirtualBox VMs"
{ directory = ".gnupg"; mode = "0700"; }
{ directory = ".ssh"; mode = "0700"; }
{ directory = ".nixops"; mode = "0700"; }
{ directory = ".local/share/keyrings"; mode = "0700"; }
".local/share/direnv"
];
files = [
".screenrc"
];
};
}
'';
};
files = mkOption {
type = listOf (either str rootFile);
default = [ ];
example = [
"/etc/machine-id"
"/etc/nix/id_rsa"
];
description = ''
Files that should be stored in persistent storage.
'';
apply =
map (file:
if isString file then
{
inherit file persistentStoragePath;
parentDirectory = defaultPerms;
}
else
file);
};
directories = mkOption {
type = listOf (either str rootDir);
default = [ ];
example = [
"/var/log"
"/var/lib/bluetooth"
"/var/lib/nixos"
"/var/lib/systemd/coredump"
"/etc/NetworkManager/system-connections"
];
description = ''
Directories to bind mount to persistent storage.
'';
apply =
map (directory:
if isString directory then
defaultPerms // {
inherit directory persistentStoragePath;
}
else
directory);
};
hideMounts = mkOption {
type = bool;
default = false;
example = true;
description = ''
Whether to hide bind mounts from showing up as mounted drives.
'';
};
enableDebugging = mkOption {
type = bool;
default = false;
internal = true;
description = ''
Enable debug trace output when running
scripts. You only need to enable this if asked
to.
'';
};
};
config =
let
allUsers = zipAttrsWith (_name: flatten) (attrValues config.users);
in
{
files = allUsers.files or [ ];
directories = allUsers.directories or [ ];
};
}
)
);
2020-12-13 15:16:35 +00:00
description = ''
A set of persistent storage location submodules listing the
files and directories to link to their respective persistent
storage location.
Each attribute name should be the full path to a persistent
storage location.
2020-12-13 15:16:35 +00:00
For detailed usage, check the <link
xlink:href="https://github.com/nix-community/impermanence">documentation</link>.
'';
example = literalExpression ''
{
"/persistent" = {
directories = [
"/var/log"
"/var/lib/bluetooth"
"/var/lib/nixos"
"/var/lib/systemd/coredump"
"/etc/NetworkManager/system-connections"
{ directory = "/var/lib/colord"; user = "colord"; group = "colord"; mode = "u=rwx,g=rx,o="; }
];
files = [
"/etc/machine-id"
{ file = "/etc/nix/id_rsa"; parentDirectory = { mode = "u=rwx,g=,o="; }; }
];
};
users.talyz = { ... }; # See the dedicated example
}
'';
2020-06-04 21:42:16 +00:00
};
2022-02-11 01:12:23 +00:00
# Forward declare a dummy option for VM filesystems since the real one won't exist
# unless the VM module is actually imported.
virtualisation.fileSystems = mkOption { };
2020-06-04 21:42:16 +00:00
};
config = {
systemd.services =
2020-06-04 21:42:16 +00:00
let
mkPersistFileService = { file, persistentStoragePath, ... }:
let
targetFile = escapeShellArg (concatPaths [ persistentStoragePath file ]);
mountPoint = escapeShellArg file;
enableDebugging = escapeShellArg cfg.${persistentStoragePath}.enableDebugging;
in
{
"persist-${sanitizeName targetFile}" = {
description = "Bind mount or link ${targetFile} to ${mountPoint}";
wantedBy = [ "local-fs.target" ];
before = [ "local-fs.target" ];
2022-01-31 02:22:23 +00:00
path = [ pkgs.util-linux ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${mountFile} ${mountPoint} ${targetFile} ${enableDebugging}";
ExecStop = pkgs.writeShellScript "unbindOrUnlink-${sanitizeName targetFile}" ''
set -eu
if [[ -L ${mountPoint} ]]; then
rm ${mountPoint}
else
umount ${mountPoint}
rm ${mountPoint}
fi
'';
};
};
};
2020-06-04 21:42:16 +00:00
in
foldl' recursiveUpdate { } (map mkPersistFileService files);
2020-06-04 21:42:16 +00:00
2022-02-11 01:12:23 +00:00
fileSystems = bindMounts;
# So the mounts still make it into a VM built from `system.build.vm`
virtualisation.fileSystems = bindMounts;
2020-06-04 21:42:16 +00:00
system.activationScripts =
let
# Script to create directories in persistent and ephemeral
# storage. The directory structure's mode and ownership mirror
# those of persistentStoragePath/dir.
createDirectories = pkgs.runCommand "impermanence-create-directories" { buildInputs = [ pkgs.bash ]; } ''
cp ${./create-directories.bash} $out
patchShebangs $out
'';
2020-06-04 21:42:16 +00:00
mkDirWithPerms = { directory, persistentStoragePath, user, group, mode }:
let
args = [
persistentStoragePath
directory
user
group
mode
cfg.${persistentStoragePath}.enableDebugging
];
in
''
${createDirectories} ${escapeShellArgs args}
'';
2020-06-07 08:43:44 +00:00
# Build an activation script which creates all persistent
# storage directories we want to bind mount.
dirCreationScript =
let
inherit directories;
fileDirectories = unique (map
(f:
{
directory = dirOf f.file;
inherit (f) persistentStoragePath;
} // f.parentDirectory)
files);
in
pkgs.writeShellScript "impermanence-run-create-directories" ''
_status=0
trap "_status=1" ERR
${concatMapStrings mkDirWithPerms (directories ++ fileDirectories)}
exit $_status
'';
mkPersistFile = { file, persistentStoragePath, ... }:
let
mountPoint = file;
targetFile = concatPaths [ persistentStoragePath file ];
args = escapeShellArgs [
mountPoint
targetFile
cfg.${persistentStoragePath}.enableDebugging
];
in
''
${mountFile} ${args}
'';
persistFileScript =
pkgs.writeShellScript "impermanence-persist-files" ''
_status=0
trap "_status=1" ERR
${concatMapStrings mkPersistFile files}
exit $_status
'';
2020-06-04 21:42:16 +00:00
in
{
"createPersistentStorageDirs" = {
deps = [ "users" "groups" ];
text = "${dirCreationScript}";
};
"persist-files" = {
deps = [ "createPersistentStorageDirs" ];
text = "${persistFileScript}";
};
};
2020-06-04 21:42:16 +00:00
assertions =
let
markedNeededForBoot = cond: fs:
if config.fileSystems ? ${fs} then
config.fileSystems.${fs}.neededForBoot == cond
else
cond;
persistentStoragePaths = attrNames cfg;
usersPerPath = allPersistentStoragePaths.users;
homeDirOffenders =
filterAttrs
(n: v: (v.home != config.users.users.${n}.home));
2020-06-05 17:37:07 +00:00
in
[
2020-06-08 16:40:35 +00:00
{
# 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:
2020-06-04 21:42:16 +00:00
${concatStringsSep "\n " offenders}
'';
}
{
assertion = all (users: (homeDirOffenders users) == { }) usersPerPath;
message =
let
offendersPerPath = filter (users: (homeDirOffenders users) != { }) usersPerPath;
offendersText =
concatMapStringsSep
"\n "
(offenders:
concatMapStringsSep
"\n "
(n: "${n}: ${offenders.${n}.home} != ${config.users.users.${n}.home}")
(attrNames offenders))
offendersPerPath;
in
''
environment.persistence:
Users and home doesn't match:
${offendersText}
You probably want to set each
environment.persistence.<path>.users.<user>.home to
match the respective user's home directory as
defined by users.users.<user>.home.
'';
}
{
assertion = duplicates (catAttrs "file" files) == [ ];
message =
let
offenders = duplicates (catAttrs "file" files);
in
''
environment.persistence:
The following files were specified two or more
times:
${concatStringsSep "\n " offenders}
'';
}
{
assertion = duplicates (catAttrs "directory" directories) == [ ];
message =
let
offenders = duplicates (catAttrs "directory" directories);
in
''
environment.persistence:
The following directories were specified two or more
times:
${concatStringsSep "\n " offenders}
'';
}
2020-06-04 21:42:16 +00:00
];
};
}