-
-
Notifications
You must be signed in to change notification settings - Fork 75
/
nixos.nix
214 lines (188 loc) · 7.77 KB
/
nixos.nix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
{ 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
# Script to 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.
createDirectories = pkgs.writeShellScript "impermanence-create-directories"
''
# 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
# Get inputs from command line arguments
sourceBase="$1"
target="$2"
# 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
'';
mkDirWithPerms = persistentStoragePath: dir: ''
${createDirectories} "${persistentStoragePath}" "${dir}"
'';
# 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}
'';
}
];
};
}