mirror of
https://github.com/nix-community/impermanence
synced 2024-11-10 05:44:17 +00:00
2d575226bf
The logic for detecting whether a file or directory is specified multiple times was not updated to account for the recent file/filePath and directory/dirPath changes. This can lead to spurious failures like the following if the same home-relative path is persisted for different users: Failed assertions: - environment.persistence: The following directories were specified two or more times: .cache .config .local/share .local/state .gnupg .ssh In addition, the assertion may falsely not trigger in the contrived situation where the same entity is persisted both in a per-user configuration (using the relative path) and the global configuration (using the absolute path). Fix these situations by checking the absolute paths again using filePath and dirPath.
717 lines
25 KiB
Nix
717 lines
25 KiB
Nix
{ pkgs, config, lib, ... }:
|
|
|
|
let
|
|
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
|
|
optionalString
|
|
elem
|
|
mapAttrs
|
|
;
|
|
|
|
inherit (pkgs.callPackage ./lib.nix { })
|
|
splitPath
|
|
dirListToPath
|
|
concatPaths
|
|
sanitizeName
|
|
duplicates
|
|
parentsOf
|
|
;
|
|
|
|
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
|
|
'';
|
|
|
|
# Create fileSystems bind mount entry.
|
|
mkBindMountNameValuePair = { dirPath, persistentStoragePath, ... }: {
|
|
name = concatPaths [ "/" dirPath ];
|
|
value = {
|
|
device = concatPaths [ persistentStoragePath dirPath ];
|
|
noCheck = true;
|
|
options = [ "bind" ]
|
|
++ optional cfg.${persistentStoragePath}.hideMounts "x-gvfs-hide";
|
|
depends = [ persistentStoragePath ];
|
|
};
|
|
};
|
|
|
|
# Create all fileSystems bind mount entries for a specific
|
|
# persistent storage path.
|
|
bindMounts = listToAttrs (map mkBindMountNameValuePair directories);
|
|
in
|
|
{
|
|
options = {
|
|
|
|
environment.persistence = mkOption {
|
|
default = { };
|
|
type =
|
|
let
|
|
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.
|
|
'';
|
|
};
|
|
home = mkOption {
|
|
type = nullOr path;
|
|
default = null;
|
|
internal = true;
|
|
description = ''
|
|
The path to the home directory the file is
|
|
placed within.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
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 // {
|
|
# See comment in dirOpts below for an explanation.
|
|
defaultPerms = mapAttrs (_: x: x // { internal = true; }) dirPermsOpts;
|
|
};
|
|
filePath = mkOption {
|
|
type = path;
|
|
internal = true;
|
|
};
|
|
};
|
|
};
|
|
dirOpts = {
|
|
options = {
|
|
directory = mkOption {
|
|
type = str;
|
|
description = ''
|
|
The path to the directory.
|
|
'';
|
|
};
|
|
# Save the default permissions at the level the
|
|
# directory resides. This used when creating its
|
|
# parent directories, giving them reasonable
|
|
# default permissions unaffected by the
|
|
# directory's own.
|
|
defaultPerms = mapAttrs (_: x: x // { internal = true; }) dirPermsOpts;
|
|
dirPath = mkOption {
|
|
type = path;
|
|
internal = true;
|
|
};
|
|
} // dirPermsOpts;
|
|
};
|
|
rootFile = submodule [
|
|
commonOpts
|
|
fileOpts
|
|
({ config, ... }: {
|
|
parentDirectory = mkDefault (defaultPerms // { inherit defaultPerms; });
|
|
filePath = mkDefault config.file;
|
|
})
|
|
];
|
|
rootDir = submodule ([
|
|
commonOpts
|
|
dirOpts
|
|
({ config, ... }: {
|
|
defaultPerms = mkDefault defaultPerms;
|
|
dirPath = mkDefault config.directory;
|
|
})
|
|
] ++ (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;
|
|
};
|
|
fileConfig =
|
|
{ config, ... }:
|
|
{
|
|
parentDirectory = mkDefault (userDefaultPerms // { defaultPerms = userDefaultPerms; });
|
|
filePath =
|
|
if config.home != null then
|
|
concatPaths [ config.home config.file ]
|
|
else
|
|
config.file;
|
|
};
|
|
userFile = submodule [
|
|
commonOpts
|
|
fileOpts
|
|
{ inherit (config) home; }
|
|
fileConfig
|
|
];
|
|
dirConfig =
|
|
{ config, ... }:
|
|
{
|
|
defaultPerms = mkDefault userDefaultPerms;
|
|
dirPath =
|
|
if config.home != null then
|
|
concatPaths [ config.home config.directory ]
|
|
else
|
|
config.directory;
|
|
};
|
|
userDir = submodule ([
|
|
commonOpts
|
|
dirOpts
|
|
{ inherit (config) home; }
|
|
dirConfig
|
|
] ++ (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 (coercedTo str (f: { file = f; }) userFile);
|
|
default = [ ];
|
|
example = [
|
|
".screenrc"
|
|
];
|
|
description = ''
|
|
Files that should be stored in
|
|
persistent storage.
|
|
'';
|
|
};
|
|
|
|
directories = mkOption {
|
|
type = listOf (coercedTo str (d: { directory = d; }) userDir);
|
|
default = [ ];
|
|
example = [
|
|
"Downloads"
|
|
"Music"
|
|
"Pictures"
|
|
"Documents"
|
|
"Videos"
|
|
];
|
|
description = ''
|
|
Directories to bind mount to
|
|
persistent storage.
|
|
'';
|
|
};
|
|
};
|
|
}
|
|
)
|
|
);
|
|
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 (coercedTo str (f: { file = f; }) rootFile);
|
|
default = [ ];
|
|
example = [
|
|
"/etc/machine-id"
|
|
"/etc/nix/id_rsa"
|
|
];
|
|
description = ''
|
|
Files that should be stored in persistent storage.
|
|
'';
|
|
};
|
|
|
|
directories = mkOption {
|
|
type = listOf (coercedTo str (d: { directory = d; }) 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.
|
|
'';
|
|
};
|
|
|
|
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 [ ];
|
|
};
|
|
}
|
|
)
|
|
);
|
|
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.
|
|
|
|
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
|
|
}
|
|
'';
|
|
};
|
|
|
|
# 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 { };
|
|
};
|
|
|
|
config = {
|
|
systemd.services =
|
|
let
|
|
mkPersistFileService = { filePath, persistentStoragePath, ... }:
|
|
let
|
|
targetFile = escapeShellArg (concatPaths [ persistentStoragePath filePath ]);
|
|
mountPoint = escapeShellArg filePath;
|
|
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" ];
|
|
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
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
in
|
|
foldl' recursiveUpdate { } (map mkPersistFileService files);
|
|
|
|
fileSystems = bindMounts;
|
|
# So the mounts still make it into a VM built from `system.build.vm`
|
|
virtualisation.fileSystems = bindMounts;
|
|
|
|
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
|
|
'';
|
|
|
|
mkDirWithPerms = { dirPath, persistentStoragePath, user, group, mode, ... }:
|
|
let
|
|
args = [
|
|
persistentStoragePath
|
|
dirPath
|
|
user
|
|
group
|
|
mode
|
|
cfg.${persistentStoragePath}.enableDebugging
|
|
];
|
|
in
|
|
''
|
|
${createDirectories} ${escapeShellArgs args}
|
|
'';
|
|
|
|
# Build an activation script which creates all persistent
|
|
# storage directories we want to bind mount.
|
|
dirCreationScript =
|
|
let
|
|
# The parent directories of files.
|
|
fileDirs = unique (map
|
|
(f:
|
|
rec {
|
|
directory = dirOf f.file;
|
|
dirPath =
|
|
if f.home != null then
|
|
concatPaths [ f.home directory ]
|
|
else
|
|
directory;
|
|
inherit (f) persistentStoragePath home;
|
|
} // f.parentDirectory)
|
|
files);
|
|
|
|
# All the directories actually listed by the user and the
|
|
# parent directories of listed files.
|
|
explicitDirs = directories ++ fileDirs;
|
|
|
|
# Home directories have to be handled specially, since
|
|
# they're at the permissions boundary where they
|
|
# themselves should be owned by the user and have stricter
|
|
# permissions than regular directories, whereas its parent
|
|
# should be owned by root and have regular permissions.
|
|
#
|
|
# This simply collects all the home directories and sets
|
|
# the appropriate permissions and ownership.
|
|
homeDirs =
|
|
foldl'
|
|
(state: dir:
|
|
let
|
|
defaultPerms = {
|
|
mode = "0755";
|
|
user = "root";
|
|
group = "root";
|
|
};
|
|
homeDir = {
|
|
directory = dir.home;
|
|
dirPath = dir.home;
|
|
home = null;
|
|
mode = "0700";
|
|
user = dir.user;
|
|
group = users.${dir.user}.group;
|
|
inherit defaultPerms;
|
|
inherit (dir) persistentStoragePath;
|
|
};
|
|
in
|
|
if dir.home != null then
|
|
if !(elem homeDir state) then
|
|
state ++ [ homeDir ]
|
|
else
|
|
state
|
|
else
|
|
state)
|
|
[ ]
|
|
explicitDirs;
|
|
|
|
# Generate entries for all parent directories of the
|
|
# argument directories, listed in the order they need to
|
|
# be created. The parent directories are assigned default
|
|
# permissions.
|
|
mkParentDirs = dirs:
|
|
let
|
|
# Create a new directory item from `dir`, the child
|
|
# directory item to inherit properties from and
|
|
# `path`, the parent directory path.
|
|
mkParent = dir: path: {
|
|
directory = path;
|
|
dirPath =
|
|
if dir.home != null then
|
|
concatPaths [ dir.home path ]
|
|
else
|
|
path;
|
|
inherit (dir) persistentStoragePath home;
|
|
inherit (dir.defaultPerms) user group mode;
|
|
};
|
|
# Create new directory items for all parent
|
|
# directories of a directory.
|
|
mkParents = dir:
|
|
map (mkParent dir) (parentsOf dir.directory);
|
|
in
|
|
unique (flatten (map mkParents dirs));
|
|
|
|
# Parent directories of home folders. This is usually only
|
|
# /home, unless the user's home is in a non-standard
|
|
# location.
|
|
homeDirParents = mkParentDirs homeDirs;
|
|
|
|
# Parent directories of all explicitly listed directories.
|
|
parentDirs = mkParentDirs explicitDirs;
|
|
|
|
# All directories in the order they should be created.
|
|
allDirs = homeDirParents ++ homeDirs ++ parentDirs ++ explicitDirs;
|
|
in
|
|
pkgs.writeShellScript "impermanence-run-create-directories" ''
|
|
_status=0
|
|
trap "_status=1" ERR
|
|
${concatMapStrings mkDirWithPerms allDirs}
|
|
exit $_status
|
|
'';
|
|
|
|
mkPersistFile = { filePath, persistentStoragePath, ... }:
|
|
let
|
|
mountPoint = filePath;
|
|
targetFile = concatPaths [ persistentStoragePath filePath ];
|
|
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
|
|
'';
|
|
in
|
|
{
|
|
"createPersistentStorageDirs" = {
|
|
deps = [ "users" "groups" ];
|
|
text = "${dirCreationScript}";
|
|
};
|
|
"persist-files" = {
|
|
deps = [ "createPersistentStorageDirs" ];
|
|
text = "${persistFileScript}";
|
|
};
|
|
};
|
|
|
|
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));
|
|
in
|
|
[
|
|
{
|
|
# 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}
|
|
'';
|
|
}
|
|
{
|
|
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 "filePath" files) == [ ];
|
|
message =
|
|
let
|
|
offenders = duplicates (catAttrs "filePath" files);
|
|
in
|
|
''
|
|
environment.persistence:
|
|
The following files were specified two or more
|
|
times:
|
|
${concatStringsSep "\n " offenders}
|
|
'';
|
|
}
|
|
{
|
|
assertion = duplicates (catAttrs "dirPath" directories) == [ ];
|
|
message =
|
|
let
|
|
offenders = duplicates (catAttrs "dirPath" directories);
|
|
in
|
|
''
|
|
environment.persistence:
|
|
The following directories were specified two or more
|
|
times:
|
|
${concatStringsSep "\n " offenders}
|
|
'';
|
|
}
|
|
];
|
|
};
|
|
|
|
}
|