-
Notifications
You must be signed in to change notification settings - Fork 15
/
lib.nix
370 lines (323 loc) · 12.9 KB
/
lib.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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
{ nixpkgs # The nixpkgs flake
, self # The system-manager flake
, nixos # The path to the nixos dir from nixpkgs
,
}:
let
inherit (nixpkgs) lib;
in
{
# Function that can be used when defining inline modules to get better location
# reporting in module-system errors.
# Usage example:
# { _file = "${printAttrPos (builtins.unsafeGetAttrPos "a" { a = null; })}: inline module"; }
printAttrPos = { file, line, column }: "${file}:${toString line}:${toString column}";
makeSystemConfig =
{ modules
, extraSpecialArgs ? { }
}:
let
# Module that sets additional module arguments
extraArgsModule = { lib, config, pkgs, ... }: {
_file = "${self.lib.printAttrPos (builtins.unsafeGetAttrPos "a" { a = null; })}: inline module";
_module.args = {
pkgs = nixpkgs.legacyPackages.${config.nixpkgs.hostPlatform};
utils = import "${nixos}/lib/utils.nix" {
inherit lib config pkgs;
};
# Pass the wrapped system-manager binary down
inherit (self.packages.${config.nixpkgs.hostPlatform}) system-manager;
};
};
config = (lib.evalModules {
specialArgs = { nixosModulesPath = "${nixos}/modules"; } // extraSpecialArgs;
modules = [
extraArgsModule
./modules
] ++ modules;
}).config;
# Get the system as it was defined in the modules.
system = config.nixpkgs.hostPlatform;
pkgs = nixpkgs.legacyPackages.${system};
returnIfNoAssertions = drv:
let
failedAssertions = map (x: x.message) (lib.filter (x: !x.assertion) config.assertions);
in
if failedAssertions != [ ]
then throw "\nFailed assertions:\n${lib.concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
else lib.showWarnings config.warnings drv;
servicesPath = pkgs.writeTextFile {
name = "services";
destination = "/services.json";
text = lib.generators.toJSON { } config.build.services;
};
etcPath = pkgs.writeTextFile {
name = "etcFiles";
destination = "/etcFiles.json";
text = lib.generators.toJSON { } { inherit (config.build.etc) entries staticEnv; };
};
linkFarmNestedEntryFromDrv = dirs: drv: {
name = lib.concatStringsSep "/" (dirs ++ [ "${drv.name}" ]);
path = drv;
};
linkFarmEntryFromDrv = linkFarmNestedEntryFromDrv [ ];
linkFarmBinEntryFromDrv = linkFarmNestedEntryFromDrv [ "bin" ];
toplevel =
let
scripts = lib.mapAttrsToList
(_: script: linkFarmBinEntryFromDrv script)
config.build.scripts;
entries = [
(linkFarmEntryFromDrv servicesPath)
(linkFarmEntryFromDrv etcPath)
] ++ scripts;
addPassthru = drv: drv.overrideAttrs (prevAttrs: {
passthru = (prevAttrs.passthru or { }) // {
inherit config;
};
});
in
addPassthru (pkgs.linkFarm "system-manager" entries);
in
returnIfNoAssertions toplevel;
# Careful since we do not have the nix store yet when this service runs,
# so we cannot use pkgs.writeTest or pkgs.writeShellScript for instance,
# since their results would refer to the store
mount_store = { pkgs, pathsToRegister }:
let
pathRegistrationInfo = "${pkgs.closureInfo { rootPaths = pathsToRegister; }}/registration";
in
pkgs.writeText "mount-store.service" ''
[Service]
Type = oneshot
ExecStart = mkdir -p /nix/.ro-store
ExecStart = mount -t 9p -o defaults,trans=virtio,version=9p2000.L,cache=loose,msize=${toString (256 * 1024 * 1024)} nix-store /nix/.ro-store
ExecStart = mkdir -p -m 0755 /nix/.rw-store/ /nix/store
ExecStart = mount -t tmpfs tmpfs /nix/.rw-store
ExecStart = mkdir -p -m 0755 /nix/.rw-store/store /nix/.rw-store/work
ExecStart = mount -t overlay overlay /nix/store -o lowerdir=/nix/.ro-store,upperdir=/nix/.rw-store/store,workdir=/nix/.rw-store/work
# Register the required paths in the nix DB.
# The store has been mounted at this point, to we can use writeShellScript now.
ExecStart = ${pkgs.writeShellScript "execstartpost-script" ''
${lib.getBin pkgs.nix}/bin/nix-store --load-db < ${pathRegistrationInfo}
''}
[Install]
WantedBy = multi-user.target
'';
# Backdoor service that exposes a root shell through a socket to the test instrumentation framework
backdoor = { pkgs }:
pkgs.writeText "backdoor.service" ''
[Unit]
Requires = dev-hvc0.device dev-ttyS0.device mount-store.service
After = dev-hvc0.device dev-ttyS0.device mount-store.service
# Keep this unit active when we switch to rescue mode for instance
IgnoreOnIsolate = true
[Service]
ExecStart = ${pkgs.writeShellScript "backdoor-start-script" ''
set -euo pipefail
export USER=root
export HOME=/root
export DISPLAY=:0.0
# TODO: do we actually need to source /etc/profile ?
# Unbound vars cause the service to crash
#source /etc/profile
# Don't use a pager when executing backdoor
# actions. Because we use a tty, commands like systemctl
# or nix-store get confused into thinking they're running
# interactively.
export PAGER=
cd /tmp
exec < /dev/hvc0 > /dev/hvc0
while ! exec 2> /dev/ttyS0; do sleep 0.1; done
echo "connecting to host..." >&2
stty -F /dev/hvc0 raw -echo # prevent nl -> cr/nl conversion
# This line is essential since it signals to the test driver that the
# shell is ready.
# See: the connect method in the Machine class.
echo "Spawning backdoor root shell..."
# Passing the terminal device makes bash run non-interactively.
# Otherwise we get errors on the terminal because bash tries to
# setup things like job control.
PS1= exec /usr/bin/env bash --norc /dev/hvc0
''}
KillSignal = SIGHUP
[Install]
WantedBy = multi-user.target
'';
prepareUbuntuImage = { hostPkgs, nodeConfig, image, extraPathsToRegister ? [ ] }:
let
pkgs = hostPkgs;
img = pkgs.fetchurl {
inherit (image) hash;
url = "https://cloud-images.ubuntu.com/releases/${image.releaseName}/release-${image.releaseTimeStamp}/${image.name}";
};
resultImg = "./image.qcow2";
# The nix store paths that need to be added to the nix DB for this node.
pathsToRegister = [ nodeConfig.systemConfig ] ++ extraPathsToRegister;
in
pkgs.runCommand "${image.name}-system-manager-vm-test.qcow2" { } ''
# We will modify the VM image, so we need a mutable copy
install -m777 ${img} ${resultImg}
# Copy the service files here, since otherwise they end up in the VM
# with their paths including the nix hash
cp ${self.lib.backdoor { inherit pkgs; }} backdoor.service
cp ${self.lib.mount_store { inherit pkgs pathsToRegister; }} mount-store.service
#export LIBGUESTFS_DEBUG=1 LIBGUESTFS_TRACE=1
${lib.concatStringsSep " \\\n" [
"${pkgs.guestfs-tools}/bin/virt-customize"
"-a ${resultImg}"
"--smp 2"
"--memsize 256"
"--no-network"
"--copy-in backdoor.service:/etc/systemd/system"
"--copy-in mount-store.service:/etc/systemd/system"
''--link ${nodeConfig.systemConfig}:/system-manager-profile''
"--run"
(pkgs.writeShellScript "run-script" ''
# Clear the root password
passwd -d root
# Don't spawn ttys on these devices, they are used for test instrumentation
systemctl mask serial-getty@ttyS0.service
systemctl mask serial-getty@hvc0.service
# Speed up the boot process
systemctl mask snapd.service
systemctl mask snapd.socket
systemctl mask snapd.seeded.service
# We have no network in the test VMs, avoid an error on bootup
systemctl mask ssh.service
systemctl mask ssh.socket
systemctl enable backdoor.service
'')
]};
cp ${resultImg} $out
'';
mkTestPreamble =
{ node
, profile
, action
}: ''
${node}.succeed("/${profile}/bin/${action} 2>&1 | tee /tmp/output.log")
${node}.succeed("! grep -F 'ERROR' /tmp/output.log")
'';
activateProfileSnippet = { node, profile ? "system-manager-profile" }:
self.lib.mkTestPreamble {
inherit node profile;
action = "activate";
};
deactivateProfileSnippet = { node, profile ? "system-manager-profile" }:
self.lib.mkTestPreamble {
inherit node profile;
action = "deactivate";
};
prepopulateProfileSnippet = { node, profile ? "system-manager-profile" }:
self.lib.mkTestPreamble {
inherit node profile;
action = "prepopulate";
};
make-vm-test =
name:
{ system
, modules
}:
let
hostPkgs = nixpkgs.legacyPackages.${system};
config = (lib.evalModules {
specialArgs = { system-manager = self; };
modules = [
../test/nix/test-driver/modules
{
_file = "${self.lib.printAttrPos (builtins.unsafeGetAttrPos "a" { a = null; })}: inline module";
inherit hostPkgs;
}
] ++ modules;
}).config;
nodes = map runVmScript (lib.attrValues config.nodes);
runVmScript = node:
# The test driver extracts the name of the node from the name of the
# VM script, so it's important here to stick to the naming scheme expected
# by the test driver.
hostPkgs.writeShellScript "run-${node.system.name}-vm" ''
set -eo pipefail
export PATH=${lib.makeBinPath [ hostPkgs.coreutils ]}''${PATH:+:}$PATH
# Create a directory for storing temporary data of the running VM.
if [ -z "$TMPDIR" ] || [ -z "$USE_TMPDIR" ]; then
TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir)
fi
# Create a directory for exchanging data with the VM.
mkdir -p "$TMPDIR/xchg"
cd "$TMPDIR"
# Start QEMU.
# We might need to be smarter about the QEMU binary to run when we want to
# support architectures other than x86_64.
# See qemu-common.nix in nixpkgs.
${lib.concatStringsSep "\\\n " [
"exec ${lib.getBin hostPkgs.qemu_test}/bin/qemu-kvm"
"-device virtio-rng-pci"
"-cpu max"
"-name ${node.system.name}"
"-m ${toString node.virtualisation.memorySize}"
"-smp ${toString node.virtualisation.cpus}"
"-drive file=${node.virtualisation.rootImage},format=qcow2"
"-device virtio-net-pci,netdev=net0"
"-netdev user,id=net0"
"-virtfs local,security_model=passthrough,id=fsdev1,path=/nix/store,readonly=on,mount_tag=nix-store"
(lib.concatStringsSep "\\\n "
(lib.mapAttrsToList
(tag: share: "-virtfs local,path=${share.source},security_model=none,mount_tag=${tag}")
node.virtualisation.sharedDirectories))
"-snapshot"
"-nographic"
"$QEMU_OPTS"
"$@"
]};
'';
test-driver =
let
upstream = hostPkgs.callPackage "${nixpkgs}/nixos/lib/test-driver" { };
in
upstream.overrideAttrs (_: {
# Try to apply the patch for backwards compat.
# It is included upstream starting from NixOS 23.05.
# github.com/NixOS/nixpkgs#228220
postPatch =
let
patch = "${lib.getBin hostPkgs.patch}/bin/patch";
patchFile = ../test/0001-nixos-test-driver-include-a-timeout-for-the-recv-cal.patch;
in
''
echo "Try to apply patch ${patchFile}..."
if grep --quiet --fixed-strings "bash" test_driver/machine.py; then
echo "Patch already present, ignoring..."
else
${patch} -p1 < ${patchFile}
fi
'';
});
runTest = { nodes, vlans, testScript, extraDriverArgs }: ''
${lib.getBin test-driver}/bin/nixos-test-driver \
${extraDriverArgs} \
--start-scripts ${lib.concatStringsSep " " nodes} \
--vlans ${lib.concatStringsSep " " vlans} \
-- ${hostPkgs.writeText "test-script" config.testScript}
'';
defaultTest = { extraDriverArgs ? "" }: runTest {
inherit extraDriverArgs nodes;
inherit (config) testScript;
vlans = [ "1" ];
};
in
hostPkgs.stdenv.mkDerivation (finalAttrs: {
inherit name;
requiredSystemFeatures = [ "kvm" "nixos-test" ];
buildCommand = ''
${defaultTest {}}
touch $out
'';
passthru = {
driverInteractive = hostPkgs.writeShellScriptBin "run-vm"
(defaultTest {
extraDriverArgs = "--interactive";
});
};
});
}