about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/development/nixos-tests.xml6
-rw-r--r--nixos/doc/manual/development/running-nixos-tests-interactively.section.md44
-rw-r--r--nixos/doc/manual/development/running-nixos-tests-interactively.xml49
-rw-r--r--nixos/doc/manual/development/running-nixos-tests.section.md31
-rw-r--r--nixos/doc/manual/development/running-nixos-tests.xml36
-rw-r--r--nixos/doc/manual/development/writing-nixos-tests.section.md301
-rw-r--r--nixos/doc/manual/development/writing-nixos-tests.xml517
-rw-r--r--nixos/doc/manual/from_md/development/running-nixos-tests-interactively.section.xml50
-rw-r--r--nixos/doc/manual/from_md/development/running-nixos-tests.section.xml34
-rw-r--r--nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml526
-rw-r--r--nixos/modules/config/shells-environment.nix12
-rw-r--r--nixos/modules/security/systemd-confinement.nix2
-rw-r--r--nixos/modules/services/audio/slimserver.nix1
-rw-r--r--nixos/modules/services/backup/sanoid.nix88
-rw-r--r--nixos/modules/services/misc/geoipupdate.nix50
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix8
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix1
-rw-r--r--nixos/modules/services/networking/tailscale.nix2
-rw-r--r--nixos/modules/services/web-apps/discourse.nix5
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py2
-rw-r--r--nixos/tests/sanoid.nix2
-rw-r--r--nixos/tests/vault.nix4
-rw-r--r--nixos/tests/zsh-history.nix2
23 files changed, 1094 insertions, 679 deletions
diff --git a/nixos/doc/manual/development/nixos-tests.xml b/nixos/doc/manual/development/nixos-tests.xml
index 2695082e3867..702fc03f6686 100644
--- a/nixos/doc/manual/development/nixos-tests.xml
+++ b/nixos/doc/manual/development/nixos-tests.xml
@@ -13,7 +13,7 @@ xlink:href="https://github.com/NixOS/nixpkgs/tree/master/nixos/tests">nixos/test
   one or more virtual machines containing the NixOS system(s) required for the
   test.
  </para>
- <xi:include href="writing-nixos-tests.xml" />
- <xi:include href="running-nixos-tests.xml" />
- <xi:include href="running-nixos-tests-interactively.xml" />
+ <xi:include href="../from_md/development/writing-nixos-tests.section.xml" />
+ <xi:include href="../from_md/development/running-nixos-tests.section.xml" />
+ <xi:include href="../from_md/development/running-nixos-tests-interactively.section.xml" />
 </chapter>
diff --git a/nixos/doc/manual/development/running-nixos-tests-interactively.section.md b/nixos/doc/manual/development/running-nixos-tests-interactively.section.md
new file mode 100644
index 000000000000..3ba4e16e77f4
--- /dev/null
+++ b/nixos/doc/manual/development/running-nixos-tests-interactively.section.md
@@ -0,0 +1,44 @@
+# Running Tests interactively {#sec-running-nixos-tests-interactively}
+
+The test itself can be run interactively. This is particularly useful
+when developing or debugging a test:
+
+```ShellSession
+$ nix-build nixos/tests/login.nix -A driverInteractive
+$ ./result/bin/nixos-test-driver
+starting VDE switch for network 1
+>
+```
+
+You can then take any Python statement, e.g.
+
+```py
+> start_all()
+> test_script()
+> machine.succeed("touch /tmp/foo")
+> print(machine.succeed("pwd")) # Show stdout of command
+```
+
+The function `test_script` executes the entire test script and drops you
+back into the test driver command line upon its completion. This allows
+you to inspect the state of the VMs after the test (e.g. to debug the
+test script).
+
+To just start and experiment with the VMs, run:
+
+```ShellSession
+$ nix-build nixos/tests/login.nix -A driverInteractive
+$ ./result/bin/nixos-run-vms
+```
+
+The script `nixos-run-vms` starts the virtual machines defined by test.
+
+You can re-use the VM states coming from a previous run by setting the
+`--keep-vm-state` flag.
+
+```ShellSession
+$ ./result/bin/nixos-run-vms --keep-vm-state
+```
+
+The machine state is stored in the `$TMPDIR/vm-state-machinename`
+directory.
diff --git a/nixos/doc/manual/development/running-nixos-tests-interactively.xml b/nixos/doc/manual/development/running-nixos-tests-interactively.xml
deleted file mode 100644
index a6044d5f89e8..000000000000
--- a/nixos/doc/manual/development/running-nixos-tests-interactively.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook"
-        xmlns:xlink="http://www.w3.org/1999/xlink"
-        xmlns:xi="http://www.w3.org/2001/XInclude"
-        version="5.0"
-        xml:id="sec-running-nixos-tests-interactively">
- <title>Running Tests interactively</title>
-
- <para>
-  The test itself can be run interactively. This is particularly useful when
-  developing or debugging a test:
-<screen>
-<prompt>$ </prompt>nix-build nixos/tests/login.nix -A driverInteractive
-<prompt>$ </prompt>./result/bin/nixos-test-driver
-starting VDE switch for network 1
-<prompt>&gt;</prompt>
-</screen>
-  You can then take any Python statement, e.g.
-<screen>
-<prompt>&gt;</prompt> start_all()
-<prompt>&gt;</prompt> test_script()
-<prompt>&gt;</prompt> machine.succeed("touch /tmp/foo")
-<prompt>&gt;</prompt> print(machine.succeed("pwd")) # Show stdout of command
-</screen>
-  The function <command>test_script</command> executes the entire test script
-  and drops you back into the test driver command line upon its completion.
-  This allows you to inspect the state of the VMs after the test (e.g. to debug
-  the test script).
- </para>
-
- <para>
-  To just start and experiment with the VMs, run:
-<screen>
-<prompt>$ </prompt>nix-build nixos/tests/login.nix -A driverInteractive
-<prompt>$ </prompt>./result/bin/nixos-run-vms
-</screen>
-  The script <command>nixos-run-vms</command> starts the virtual machines
-  defined by test.
- </para>
-
- <para>
-   You can re-use the VM states coming from a previous run
-   by setting the <command>--keep-vm-state</command> flag.
-<screen>
-<prompt>$ </prompt>./result/bin/nixos-run-vms --keep-vm-state
-</screen>
-  The machine state is stored in the
-  <filename>$TMPDIR/vm-state-</filename><varname>machinename</varname> directory.
- </para>
-</section>
diff --git a/nixos/doc/manual/development/running-nixos-tests.section.md b/nixos/doc/manual/development/running-nixos-tests.section.md
new file mode 100644
index 000000000000..d6a456f01883
--- /dev/null
+++ b/nixos/doc/manual/development/running-nixos-tests.section.md
@@ -0,0 +1,31 @@
+# Running Tests {#sec-running-nixos-tests}
+
+You can run tests using `nix-build`. For example, to run the test
+[`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix),
+you just do:
+
+```ShellSession
+$ nix-build '<nixpkgs/nixos/tests/login.nix>'
+```
+
+or, if you don't want to rely on `NIX_PATH`:
+
+```ShellSession
+$ cd /my/nixpkgs/nixos/tests
+$ nix-build login.nix
+…
+running the VM test script
+machine: QEMU running (pid 8841)
+…
+6 out of 6 tests succeeded
+```
+
+After building/downloading all required dependencies, this will perform
+a build that starts a QEMU/KVM virtual machine containing a NixOS
+system. The virtual machine mounts the Nix store of the host; this makes
+VM creation very fast, as no disk image needs to be created. Afterwards,
+you can view a pretty-printed log of the test:
+
+```ShellSession
+$ firefox result/log.html
+```
diff --git a/nixos/doc/manual/development/running-nixos-tests.xml b/nixos/doc/manual/development/running-nixos-tests.xml
deleted file mode 100644
index e9257c907daf..000000000000
--- a/nixos/doc/manual/development/running-nixos-tests.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook"
-        xmlns:xlink="http://www.w3.org/1999/xlink"
-        xmlns:xi="http://www.w3.org/2001/XInclude"
-        version="5.0"
-        xml:id="sec-running-nixos-tests">
- <title>Running Tests</title>
-
- <para>
-  You can run tests using <command>nix-build</command>. For example, to run the
-  test
-  <filename
-xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix">login.nix</filename>,
-  you just do:
-<screen>
-<prompt>$ </prompt>nix-build '&lt;nixpkgs/nixos/tests/login.nix>'
-</screen>
-  or, if you don’t want to rely on <envar>NIX_PATH</envar>:
-<screen>
-<prompt>$ </prompt>cd /my/nixpkgs/nixos/tests
-<prompt>$ </prompt>nix-build login.nix
-…
-running the VM test script
-machine: QEMU running (pid 8841)
-…
-6 out of 6 tests succeeded
-</screen>
-  After building/downloading all required dependencies, this will perform a
-  build that starts a QEMU/KVM virtual machine containing a NixOS system. The
-  virtual machine mounts the Nix store of the host; this makes VM creation very
-  fast, as no disk image needs to be created. Afterwards, you can view a
-  pretty-printed log of the test:
-<screen>
-<prompt>$ </prompt>firefox result/log.html
-</screen>
- </para>
-</section>
diff --git a/nixos/doc/manual/development/writing-nixos-tests.section.md b/nixos/doc/manual/development/writing-nixos-tests.section.md
new file mode 100644
index 000000000000..8471e7608af9
--- /dev/null
+++ b/nixos/doc/manual/development/writing-nixos-tests.section.md
@@ -0,0 +1,301 @@
+# Writing Tests {#sec-writing-nixos-tests}
+
+A NixOS test is a Nix expression that has the following structure:
+
+```nix
+import ./make-test-python.nix {
+
+  # Either the configuration of a single machine:
+  machine =
+    { config, pkgs, ... }:
+    { configuration…
+    };
+
+  # Or a set of machines:
+  nodes =
+    { machine1 =
+        { config, pkgs, ... }: { … };
+      machine2 =
+        { config, pkgs, ... }: { … };
+      …
+    };
+
+  testScript =
+    ''
+      Python code…
+    '';
+}
+```
+
+The attribute `testScript` is a bit of Python code that executes the
+test (described below). During the test, it will start one or more
+virtual machines, the configuration of which is described by the
+attribute `machine` (if you need only one machine in your test) or by
+the attribute `nodes` (if you need multiple machines). For instance,
+[`login.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix)
+only needs a single machine to test whether users can log in
+on the virtual console, whether device ownership is correctly maintained
+when switching between consoles, and so on. On the other hand,
+[`nfs/simple.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix),
+which tests NFS client and server functionality in the
+Linux kernel (including whether locks are maintained across server
+crashes), requires three machines: a server and two clients.
+
+There are a few special NixOS configuration options for test VMs:
+
+`virtualisation.memorySize`
+
+:   The memory of the VM in megabytes.
+
+`virtualisation.vlans`
+
+:   The virtual networks to which the VM is connected. See
+    [`nat.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix)
+    for an example.
+
+`virtualisation.writableStore`
+
+:   By default, the Nix store in the VM is not writable. If you enable
+    this option, a writable union file system is mounted on top of the
+    Nix store to make it appear writable. This is necessary for tests
+    that run Nix operations that modify the store.
+
+For more options, see the module
+[`qemu-vm.nix`](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix).
+
+The test script is a sequence of Python statements that perform various
+actions, such as starting VMs, executing commands in the VMs, and so on.
+Each virtual machine is represented as an object stored in the variable
+`name` if this is also the identifier of the machine in the declarative
+config. If you didn\'t specify multiple machines using the `nodes`
+attribute, it is just `machine`. The following example starts the
+machine, waits until it has finished booting, then executes a command
+and checks that the output is more-or-less correct:
+
+```py
+machine.start()
+machine.wait_for_unit("default.target")
+if not "Linux" in machine.succeed("uname"):
+  raise Exception("Wrong OS")
+```
+
+The first line is actually unnecessary; machines are implicitly started
+when you first execute an action on them (such as `wait_for_unit` or
+`succeed`). If you have multiple machines, you can speed up the test by
+starting them in parallel:
+
+```py
+start_all()
+```
+
+The following methods are available on machine objects:
+
+`start`
+
+:   Start the virtual machine. This method is asynchronous --- it does
+    not wait for the machine to finish booting.
+
+`shutdown`
+
+:   Shut down the machine, waiting for the VM to exit.
+
+`crash`
+
+:   Simulate a sudden power failure, by telling the VM to exit
+    immediately.
+
+`block`
+
+:   Simulate unplugging the Ethernet cable that connects the machine to
+    the other machines.
+
+`unblock`
+
+:   Undo the effect of `block`.
+
+`screenshot`
+
+:   Take a picture of the display of the virtual machine, in PNG format.
+    The screenshot is linked from the HTML log.
+
+`get_screen_text_variants`
+
+:   Return a list of different interpretations of what is currently
+    visible on the machine\'s screen using optical character
+    recognition. The number and order of the interpretations is not
+    specified and is subject to change, but if no exception is raised at
+    least one will be returned.
+
+    ::: {.note}
+    This requires passing `enableOCR` to the test attribute set.
+    :::
+
+`get_screen_text`
+
+:   Return a textual representation of what is currently visible on the
+    machine\'s screen using optical character recognition.
+
+    ::: {.note}
+    This requires passing `enableOCR` to the test attribute set.
+    :::
+
+`send_monitor_command`
+
+:   Send a command to the QEMU monitor. This is rarely used, but allows
+    doing stuff such as attaching virtual USB disks to a running
+    machine.
+
+`send_key`
+
+:   Simulate pressing keys on the virtual keyboard, e.g.,
+    `send_key("ctrl-alt-delete")`.
+
+`send_chars`
+
+:   Simulate typing a sequence of characters on the virtual keyboard,
+    e.g., `send_chars("foobar\n")` will type the string `foobar`
+    followed by the Enter key.
+
+`execute`
+
+:   Execute a shell command, returning a list `(status, stdout)`.
+
+`succeed`
+
+:   Execute a shell command, raising an exception if the exit status is
+    not zero, otherwise returning the standard output. Commands are run
+    with `set -euo pipefail` set:
+
+    -   If several commands are separated by `;` and one fails, the
+        command as a whole will fail.
+
+    -   For pipelines, the last non-zero exit status will be returned
+        (if there is one, zero will be returned otherwise).
+
+    -   Dereferencing unset variables fail the command.
+
+`fail`
+
+:   Like `succeed`, but raising an exception if the command returns a zero
+    status.
+
+`wait_until_succeeds`
+
+:   Repeat a shell command with 1-second intervals until it succeeds.
+
+`wait_until_fails`
+
+:   Repeat a shell command with 1-second intervals until it fails.
+
+`wait_for_unit`
+
+:   Wait until the specified systemd unit has reached the "active"
+    state.
+
+`wait_for_file`
+
+:   Wait until the specified file exists.
+
+`wait_for_open_port`
+
+:   Wait until a process is listening on the given TCP port (on
+    `localhost`, at least).
+
+`wait_for_closed_port`
+
+:   Wait until nobody is listening on the given TCP port.
+
+`wait_for_x`
+
+:   Wait until the X11 server is accepting connections.
+
+`wait_for_text`
+
+:   Wait until the supplied regular expressions matches the textual
+    contents of the screen by using optical character recognition (see
+    `get_screen_text` and `get_screen_text_variants`).
+
+    ::: {.note}
+    This requires passing `enableOCR` to the test attribute set.
+    :::
+
+`wait_for_console_text`
+
+:   Wait until the supplied regular expressions match a line of the
+    serial console output. This method is useful when OCR is not
+    possibile or accurate enough.
+
+`wait_for_window`
+
+:   Wait until an X11 window has appeared whose name matches the given
+    regular expression, e.g., `wait_for_window("Terminal")`.
+
+`copy_from_host`
+
+:   Copies a file from host to machine, e.g.,
+    `copy_from_host("myfile", "/etc/my/important/file")`.
+
+    The first argument is the file on the host. The file needs to be
+    accessible while building the nix derivation. The second argument is
+    the location of the file on the machine.
+
+`systemctl`
+
+:   Runs `systemctl` commands with optional support for
+    `systemctl --user`
+
+    ```py
+    machine.systemctl("list-jobs --no-pager") # runs `systemctl list-jobs --no-pager`
+    machine.systemctl("list-jobs --no-pager", "any-user") # spawns a shell for `any-user` and runs `systemctl --user list-jobs --no-pager`
+    ```
+
+`shell_interact`
+
+:   Allows you to directly interact with the guest shell. This should
+    only be used during test development, not in production tests.
+    Killing the interactive session with `Ctrl-d` or `Ctrl-c` also ends
+    the guest session.
+
+To test user units declared by `systemd.user.services` the optional
+`user` argument can be used:
+
+```py
+machine.start()
+machine.wait_for_x()
+machine.wait_for_unit("xautolock.service", "x-session-user")
+```
+
+This applies to `systemctl`, `get_unit_info`, `wait_for_unit`,
+`start_job` and `stop_job`.
+
+For faster dev cycles it\'s also possible to disable the code-linters
+(this shouldn\'t be commited though):
+
+```nix
+import ./make-test-python.nix {
+  skipLint = true;
+  machine =
+    { config, pkgs, ... }:
+    { configuration…
+    };
+
+  testScript =
+    ''
+      Python code…
+    '';
+}
+```
+
+This will produce a Nix warning at evaluation time. To fully disable the
+linter, wrap the test script in comment directives to disable the Black
+linter directly (again, don\'t commit this within the Nixpkgs
+repository):
+
+```nix
+  testScript =
+    ''
+      # fmt: off
+      Python code…
+      # fmt: on
+    '';
+```
diff --git a/nixos/doc/manual/development/writing-nixos-tests.xml b/nixos/doc/manual/development/writing-nixos-tests.xml
deleted file mode 100644
index e372c66410de..000000000000
--- a/nixos/doc/manual/development/writing-nixos-tests.xml
+++ /dev/null
@@ -1,517 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook"
-        xmlns:xlink="http://www.w3.org/1999/xlink"
-        xmlns:xi="http://www.w3.org/2001/XInclude"
-        version="5.0"
-        xml:id="sec-writing-nixos-tests">
- <title>Writing Tests</title>
-
- <para>
-  A NixOS test is a Nix expression that has the following structure:
-<programlisting>
-import ./make-test-python.nix {
-
-  # Either the configuration of a single machine:
-  machine =
-    { config, pkgs, ... }:
-    { <replaceable>configuration…</replaceable>
-    };
-
-  # Or a set of machines:
-  nodes =
-    { <replaceable>machine1</replaceable> =
-        { config, pkgs, ... }: { <replaceable>…</replaceable> };
-      <replaceable>machine2</replaceable> =
-        { config, pkgs, ... }: { <replaceable>…</replaceable> };
-      …
-    };
-
-  testScript =
-    ''
-      <replaceable>Python code…</replaceable>
-    '';
-}
-</programlisting>
-  The attribute <literal>testScript</literal> is a bit of Python code that
-  executes the test (described below). During the test, it will start one or
-  more virtual machines, the configuration of which is described by the
-  attribute <literal>machine</literal> (if you need only one machine in your
-  test) or by the attribute <literal>nodes</literal> (if you need multiple
-  machines). For instance,
-  <filename
-xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix">login.nix</filename>
-  only needs a single machine to test whether users can log in on the virtual
-  console, whether device ownership is correctly maintained when switching
-  between consoles, and so on. On the other hand,
-  <filename
-xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix">nfs/simple.nix</filename>,
-  which tests NFS client and server functionality in the Linux kernel
-  (including whether locks are maintained across server crashes), requires
-  three machines: a server and two clients.
- </para>
-
- <para>
-  There are a few special NixOS configuration options for test VMs:
-<!-- FIXME: would be nice to generate this automatically. -->
-  <variablelist>
-   <varlistentry>
-    <term>
-     <option>virtualisation.memorySize</option>
-    </term>
-    <listitem>
-     <para>
-      The memory of the VM in megabytes.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>virtualisation.vlans</option>
-    </term>
-    <listitem>
-     <para>
-      The virtual networks to which the VM is connected. See
-      <filename
-    xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix">nat.nix</filename>
-      for an example.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>virtualisation.writableStore</option>
-    </term>
-    <listitem>
-     <para>
-      By default, the Nix store in the VM is not writable. If you enable this
-      option, a writable union file system is mounted on top of the Nix store
-      to make it appear writable. This is necessary for tests that run Nix
-      operations that modify the store.
-     </para>
-    </listitem>
-   </varlistentry>
-  </variablelist>
-  For more options, see the module
-  <filename
-xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix">qemu-vm.nix</filename>.
- </para>
-
- <para>
-  The test script is a sequence of Python statements that perform various
-  actions, such as starting VMs, executing commands in the VMs, and so on. Each
-  virtual machine is represented as an object stored in the variable
-  <literal><replaceable>name</replaceable></literal> if this is also the
-  identifier of the machine in the declarative config.
-  If you didn't specify multiple machines using the <literal>nodes</literal>
-  attribute, it is just <literal>machine</literal>.
-  The following example starts the machine, waits until it has finished booting,
-  then executes a command and checks that the output is more-or-less correct:
-<programlisting>
-machine.start()
-machine.wait_for_unit("default.target")
-if not "Linux" in machine.succeed("uname"):
-  raise Exception("Wrong OS")
-</programlisting>
-  The first line is actually unnecessary; machines are implicitly started when
-  you first execute an action on them (such as <literal>wait_for_unit</literal>
-  or <literal>succeed</literal>). If you have multiple machines, you can speed
-  up the test by starting them in parallel:
-<programlisting>
-start_all()
-</programlisting>
- </para>
-
- <para>
-  The following methods are available on machine objects:
-  <variablelist>
-   <varlistentry>
-    <term>
-     <methodname>start</methodname>
-    </term>
-    <listitem>
-     <para>
-      Start the virtual machine. This method is asynchronous — it does not
-      wait for the machine to finish booting.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>shutdown</methodname>
-    </term>
-    <listitem>
-     <para>
-      Shut down the machine, waiting for the VM to exit.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>crash</methodname>
-    </term>
-    <listitem>
-     <para>
-      Simulate a sudden power failure, by telling the VM to exit immediately.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>block</methodname>
-    </term>
-    <listitem>
-     <para>
-      Simulate unplugging the Ethernet cable that connects the machine to the
-      other machines.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>unblock</methodname>
-    </term>
-    <listitem>
-     <para>
-      Undo the effect of <methodname>block</methodname>.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>screenshot</methodname>
-    </term>
-    <listitem>
-     <para>
-      Take a picture of the display of the virtual machine, in PNG format. The
-      screenshot is linked from the HTML log.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>get_screen_text_variants</methodname>
-    </term>
-    <listitem>
-     <para>
-      Return a list of different interpretations of what is currently visible
-      on the machine's screen using optical character recognition. The number
-      and order of the interpretations is not specified and is subject to
-      change, but if no exception is raised at least one will be returned.
-     </para>
-     <note>
-      <para>
-       This requires passing <option>enableOCR</option> to the test attribute
-       set.
-      </para>
-     </note>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>get_screen_text</methodname>
-    </term>
-    <listitem>
-     <para>
-      Return a textual representation of what is currently visible on the
-      machine's screen using optical character recognition.
-     </para>
-     <note>
-      <para>
-       This requires passing <option>enableOCR</option> to the test attribute
-       set.
-      </para>
-     </note>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>send_monitor_command</methodname>
-    </term>
-    <listitem>
-     <para>
-      Send a command to the QEMU monitor. This is rarely used, but allows doing
-      stuff such as attaching virtual USB disks to a running machine.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>send_key</methodname>
-    </term>
-    <listitem>
-     <para>
-      Simulate pressing keys on the virtual keyboard, e.g.,
-      <literal>send_key("ctrl-alt-delete")</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>send_chars</methodname>
-    </term>
-    <listitem>
-     <para>
-      Simulate typing a sequence of characters on the virtual keyboard, e.g.,
-      <literal>send_chars("foobar\n")</literal> will type the string
-      <literal>foobar</literal> followed by the Enter key.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>execute</methodname>
-    </term>
-    <listitem>
-     <para>
-      Execute a shell command, returning a list
-      <literal>(<replaceable>status</replaceable>,
-      <replaceable>stdout</replaceable>)</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>succeed</methodname>
-    </term>
-    <listitem>
-     <para>
-      Execute a shell command, raising an exception if the exit status
-      is not zero, otherwise returning the standard output. Commands
-      are run with <literal>set -euo pipefail</literal> set:
-      <itemizedlist>
-        <listitem>
-          <para>
-            If several commands are separated by <literal>;</literal>
-            and one fails, the command as a whole will fail.
-          </para>
-        </listitem>
-        <listitem>
-          <para>
-            For pipelines, the last non-zero exit status will be
-            returned (if there is one, zero will be returned
-            otherwise).
-          </para>
-        </listitem>
-        <listitem>
-          <para>
-            Dereferencing unset variables fail the command.
-          </para>
-        </listitem>
-      </itemizedlist>
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>fail</methodname>
-    </term>
-    <listitem>
-     <para>
-      Like <methodname>succeed</methodname>, but raising an exception if the
-      command returns a zero status.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_until_succeeds</methodname>
-    </term>
-    <listitem>
-     <para>
-      Repeat a shell command with 1-second intervals until it succeeds.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_until_fails</methodname>
-    </term>
-    <listitem>
-     <para>
-      Repeat a shell command with 1-second intervals until it fails.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_for_unit</methodname>
-    </term>
-    <listitem>
-     <para>
-      Wait until the specified systemd unit has reached the “active” state.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_for_file</methodname>
-    </term>
-    <listitem>
-     <para>
-      Wait until the specified file exists.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_for_open_port</methodname>
-    </term>
-    <listitem>
-     <para>
-      Wait until a process is listening on the given TCP port (on
-      <literal>localhost</literal>, at least).
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_for_closed_port</methodname>
-    </term>
-    <listitem>
-     <para>
-      Wait until nobody is listening on the given TCP port.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_for_x</methodname>
-    </term>
-    <listitem>
-     <para>
-      Wait until the X11 server is accepting connections.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_for_text</methodname>
-    </term>
-    <listitem>
-     <para>
-      Wait until the supplied regular expressions matches the textual contents
-      of the screen by using optical character recognition (see
-     <methodname>get_screen_text</methodname> and
-     <methodname>get_screen_text_variants</methodname>).
-     </para>
-     <note>
-      <para>
-       This requires passing <option>enableOCR</option> to the test attribute
-       set.
-      </para>
-     </note>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_for_console_text</methodname>
-    </term>
-    <listitem>
-     <para>
-      Wait until the supplied regular expressions match a line of the serial
-      console output. This method is useful when OCR is not possibile or
-      accurate enough.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>wait_for_window</methodname>
-    </term>
-    <listitem>
-     <para>
-      Wait until an X11 window has appeared whose name matches the given
-      regular expression, e.g., <literal>wait_for_window("Terminal")</literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>copy_from_host</methodname>
-    </term>
-    <listitem>
-     <para>
-      Copies a file from host to machine, e.g.,
-      <literal>copy_from_host("myfile", "/etc/my/important/file")</literal>.
-     </para>
-     <para>
-      The first argument is the file on the host. The file needs to be
-      accessible while building the nix derivation. The second argument is the
-      location of the file on the machine.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>systemctl</methodname>
-    </term>
-    <listitem>
-     <para>
-      Runs <literal>systemctl</literal> commands with optional support for
-      <literal>systemctl --user</literal>
-     </para>
-     <para>
-<programlisting>
-machine.systemctl("list-jobs --no-pager") # runs `systemctl list-jobs --no-pager`
-machine.systemctl("list-jobs --no-pager", "any-user") # spawns a shell for `any-user` and runs `systemctl --user list-jobs --no-pager`
-</programlisting>
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <methodname>shell_interact</methodname>
-    </term>
-    <listitem>
-     <para>
-      Allows you to directly interact with the guest shell.
-      This should only be used during test development, not in production tests.
-      Killing the interactive session with <literal>Ctrl-d</literal> or <literal>Ctrl-c</literal> also ends the guest session.
-     </para>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </para>
-
- <para>
-  To test user units declared by <literal>systemd.user.services</literal> the
-  optional <literal>user</literal> argument can be used:
-<programlisting>
-machine.start()
-machine.wait_for_x()
-machine.wait_for_unit("xautolock.service", "x-session-user")
-</programlisting>
-  This applies to <literal>systemctl</literal>, <literal>get_unit_info</literal>,
-  <literal>wait_for_unit</literal>, <literal>start_job</literal> and
-  <literal>stop_job</literal>.
- </para>
-
- <para>
-  For faster dev cycles it's also possible to disable the code-linters (this shouldn't
-  be commited though):
-<programlisting>
-import ./make-test-python.nix {
-  skipLint = true;
-  machine =
-    { config, pkgs, ... }:
-    { <replaceable>configuration…</replaceable>
-    };
-
-  testScript =
-    ''
-      <replaceable>Python code…</replaceable>
-    '';
-}
-</programlisting>
-  This will produce a Nix warning at evaluation time. To fully disable the
-  linter, wrap the test script in comment directives to disable the Black linter
-  directly (again, don't commit this within the Nixpkgs repository):
-<programlisting>
-  testScript =
-    ''
-      # fmt: off
-      <replaceable>Python code…</replaceable>
-      # fmt: on
-    '';
-</programlisting>
- </para>
-</section>
diff --git a/nixos/doc/manual/from_md/development/running-nixos-tests-interactively.section.xml b/nixos/doc/manual/from_md/development/running-nixos-tests-interactively.section.xml
new file mode 100644
index 000000000000..a2030e9c0739
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/running-nixos-tests-interactively.section.xml
@@ -0,0 +1,50 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-running-nixos-tests-interactively">
+  <title>Running Tests interactively</title>
+  <para>
+    The test itself can be run interactively. This is particularly
+    useful when developing or debugging a test:
+  </para>
+  <programlisting>
+$ nix-build nixos/tests/login.nix -A driverInteractive
+$ ./result/bin/nixos-test-driver
+starting VDE switch for network 1
+&gt;
+</programlisting>
+  <para>
+    You can then take any Python statement, e.g.
+  </para>
+  <programlisting language="python">
+&gt; start_all()
+&gt; test_script()
+&gt; machine.succeed(&quot;touch /tmp/foo&quot;)
+&gt; print(machine.succeed(&quot;pwd&quot;)) # Show stdout of command
+</programlisting>
+  <para>
+    The function <literal>test_script</literal> executes the entire test
+    script and drops you back into the test driver command line upon its
+    completion. This allows you to inspect the state of the VMs after
+    the test (e.g. to debug the test script).
+  </para>
+  <para>
+    To just start and experiment with the VMs, run:
+  </para>
+  <programlisting>
+$ nix-build nixos/tests/login.nix -A driverInteractive
+$ ./result/bin/nixos-run-vms
+</programlisting>
+  <para>
+    The script <literal>nixos-run-vms</literal> starts the virtual
+    machines defined by test.
+  </para>
+  <para>
+    You can re-use the VM states coming from a previous run by setting
+    the <literal>--keep-vm-state</literal> flag.
+  </para>
+  <programlisting>
+$ ./result/bin/nixos-run-vms --keep-vm-state
+</programlisting>
+  <para>
+    The machine state is stored in the
+    <literal>$TMPDIR/vm-state-machinename</literal> directory.
+  </para>
+</section>
diff --git a/nixos/doc/manual/from_md/development/running-nixos-tests.section.xml b/nixos/doc/manual/from_md/development/running-nixos-tests.section.xml
new file mode 100644
index 000000000000..7159b95b22b0
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/running-nixos-tests.section.xml
@@ -0,0 +1,34 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-running-nixos-tests">
+  <title>Running Tests</title>
+  <para>
+    You can run tests using <literal>nix-build</literal>. For example,
+    to run the test
+    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix"><literal>login.nix</literal></link>,
+    you just do:
+  </para>
+  <programlisting>
+$ nix-build '&lt;nixpkgs/nixos/tests/login.nix&gt;'
+</programlisting>
+  <para>
+    or, if you don’t want to rely on <literal>NIX_PATH</literal>:
+  </para>
+  <programlisting>
+$ cd /my/nixpkgs/nixos/tests
+$ nix-build login.nix
+…
+running the VM test script
+machine: QEMU running (pid 8841)
+…
+6 out of 6 tests succeeded
+</programlisting>
+  <para>
+    After building/downloading all required dependencies, this will
+    perform a build that starts a QEMU/KVM virtual machine containing a
+    NixOS system. The virtual machine mounts the Nix store of the host;
+    this makes VM creation very fast, as no disk image needs to be
+    created. Afterwards, you can view a pretty-printed log of the test:
+  </para>
+  <programlisting>
+$ firefox result/log.html
+</programlisting>
+</section>
diff --git a/nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml b/nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml
new file mode 100644
index 000000000000..83a96d5bb224
--- /dev/null
+++ b/nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml
@@ -0,0 +1,526 @@
+<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-writing-nixos-tests">
+  <title>Writing Tests</title>
+  <para>
+    A NixOS test is a Nix expression that has the following structure:
+  </para>
+  <programlisting language="bash">
+import ./make-test-python.nix {
+
+  # Either the configuration of a single machine:
+  machine =
+    { config, pkgs, ... }:
+    { configuration…
+    };
+
+  # Or a set of machines:
+  nodes =
+    { machine1 =
+        { config, pkgs, ... }: { … };
+      machine2 =
+        { config, pkgs, ... }: { … };
+      …
+    };
+
+  testScript =
+    ''
+      Python code…
+    '';
+}
+</programlisting>
+  <para>
+    The attribute <literal>testScript</literal> is a bit of Python code
+    that executes the test (described below). During the test, it will
+    start one or more virtual machines, the configuration of which is
+    described by the attribute <literal>machine</literal> (if you need
+    only one machine in your test) or by the attribute
+    <literal>nodes</literal> (if you need multiple machines). For
+    instance,
+    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix"><literal>login.nix</literal></link>
+    only needs a single machine to test whether users can log in on the
+    virtual console, whether device ownership is correctly maintained
+    when switching between consoles, and so on. On the other hand,
+    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nfs/simple.nix"><literal>nfs/simple.nix</literal></link>,
+    which tests NFS client and server functionality in the Linux kernel
+    (including whether locks are maintained across server crashes),
+    requires three machines: a server and two clients.
+  </para>
+  <para>
+    There are a few special NixOS configuration options for test VMs:
+  </para>
+  <variablelist>
+    <varlistentry>
+      <term>
+        <literal>virtualisation.memorySize</literal>
+      </term>
+      <listitem>
+        <para>
+          The memory of the VM in megabytes.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>virtualisation.vlans</literal>
+      </term>
+      <listitem>
+        <para>
+          The virtual networks to which the VM is connected. See
+          <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix"><literal>nat.nix</literal></link>
+          for an example.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>virtualisation.writableStore</literal>
+      </term>
+      <listitem>
+        <para>
+          By default, the Nix store in the VM is not writable. If you
+          enable this option, a writable union file system is mounted on
+          top of the Nix store to make it appear writable. This is
+          necessary for tests that run Nix operations that modify the
+          store.
+        </para>
+      </listitem>
+    </varlistentry>
+  </variablelist>
+  <para>
+    For more options, see the module
+    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix"><literal>qemu-vm.nix</literal></link>.
+  </para>
+  <para>
+    The test script is a sequence of Python statements that perform
+    various actions, such as starting VMs, executing commands in the
+    VMs, and so on. Each virtual machine is represented as an object
+    stored in the variable <literal>name</literal> if this is also the
+    identifier of the machine in the declarative config. If you didn't
+    specify multiple machines using the <literal>nodes</literal>
+    attribute, it is just <literal>machine</literal>. The following
+    example starts the machine, waits until it has finished booting,
+    then executes a command and checks that the output is more-or-less
+    correct:
+  </para>
+  <programlisting language="python">
+machine.start()
+machine.wait_for_unit(&quot;default.target&quot;)
+if not &quot;Linux&quot; in machine.succeed(&quot;uname&quot;):
+  raise Exception(&quot;Wrong OS&quot;)
+</programlisting>
+  <para>
+    The first line is actually unnecessary; machines are implicitly
+    started when you first execute an action on them (such as
+    <literal>wait_for_unit</literal> or <literal>succeed</literal>). If
+    you have multiple machines, you can speed up the test by starting
+    them in parallel:
+  </para>
+  <programlisting language="python">
+start_all()
+</programlisting>
+  <para>
+    The following methods are available on machine objects:
+  </para>
+  <variablelist>
+    <varlistentry>
+      <term>
+        <literal>start</literal>
+      </term>
+      <listitem>
+        <para>
+          Start the virtual machine. This method is asynchronous — it
+          does not wait for the machine to finish booting.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>shutdown</literal>
+      </term>
+      <listitem>
+        <para>
+          Shut down the machine, waiting for the VM to exit.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>crash</literal>
+      </term>
+      <listitem>
+        <para>
+          Simulate a sudden power failure, by telling the VM to exit
+          immediately.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>block</literal>
+      </term>
+      <listitem>
+        <para>
+          Simulate unplugging the Ethernet cable that connects the
+          machine to the other machines.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>unblock</literal>
+      </term>
+      <listitem>
+        <para>
+          Undo the effect of <literal>block</literal>.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>screenshot</literal>
+      </term>
+      <listitem>
+        <para>
+          Take a picture of the display of the virtual machine, in PNG
+          format. The screenshot is linked from the HTML log.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>get_screen_text_variants</literal>
+      </term>
+      <listitem>
+        <para>
+          Return a list of different interpretations of what is
+          currently visible on the machine's screen using optical
+          character recognition. The number and order of the
+          interpretations is not specified and is subject to change, but
+          if no exception is raised at least one will be returned.
+        </para>
+        <note>
+          <para>
+            This requires passing <literal>enableOCR</literal> to the
+            test attribute set.
+          </para>
+        </note>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>get_screen_text</literal>
+      </term>
+      <listitem>
+        <para>
+          Return a textual representation of what is currently visible
+          on the machine's screen using optical character recognition.
+        </para>
+        <note>
+          <para>
+            This requires passing <literal>enableOCR</literal> to the
+            test attribute set.
+          </para>
+        </note>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>send_monitor_command</literal>
+      </term>
+      <listitem>
+        <para>
+          Send a command to the QEMU monitor. This is rarely used, but
+          allows doing stuff such as attaching virtual USB disks to a
+          running machine.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>send_key</literal>
+      </term>
+      <listitem>
+        <para>
+          Simulate pressing keys on the virtual keyboard, e.g.,
+          <literal>send_key(&quot;ctrl-alt-delete&quot;)</literal>.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>send_chars</literal>
+      </term>
+      <listitem>
+        <para>
+          Simulate typing a sequence of characters on the virtual
+          keyboard, e.g.,
+          <literal>send_chars(&quot;foobar\n&quot;)</literal> will type
+          the string <literal>foobar</literal> followed by the Enter
+          key.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>execute</literal>
+      </term>
+      <listitem>
+        <para>
+          Execute a shell command, returning a list
+          <literal>(status, stdout)</literal>.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>succeed</literal>
+      </term>
+      <listitem>
+        <para>
+          Execute a shell command, raising an exception if the exit
+          status is not zero, otherwise returning the standard output.
+          Commands are run with <literal>set -euo pipefail</literal>
+          set:
+        </para>
+        <itemizedlist>
+          <listitem>
+            <para>
+              If several commands are separated by <literal>;</literal>
+              and one fails, the command as a whole will fail.
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              For pipelines, the last non-zero exit status will be
+              returned (if there is one, zero will be returned
+              otherwise).
+            </para>
+          </listitem>
+          <listitem>
+            <para>
+              Dereferencing unset variables fail the command.
+            </para>
+          </listitem>
+        </itemizedlist>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>fail</literal>
+      </term>
+      <listitem>
+        <para>
+          Like <literal>succeed</literal>, but raising an exception if
+          the command returns a zero status.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_until_succeeds</literal>
+      </term>
+      <listitem>
+        <para>
+          Repeat a shell command with 1-second intervals until it
+          succeeds.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_until_fails</literal>
+      </term>
+      <listitem>
+        <para>
+          Repeat a shell command with 1-second intervals until it fails.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_for_unit</literal>
+      </term>
+      <listitem>
+        <para>
+          Wait until the specified systemd unit has reached the
+          <quote>active</quote> state.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_for_file</literal>
+      </term>
+      <listitem>
+        <para>
+          Wait until the specified file exists.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_for_open_port</literal>
+      </term>
+      <listitem>
+        <para>
+          Wait until a process is listening on the given TCP port (on
+          <literal>localhost</literal>, at least).
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_for_closed_port</literal>
+      </term>
+      <listitem>
+        <para>
+          Wait until nobody is listening on the given TCP port.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_for_x</literal>
+      </term>
+      <listitem>
+        <para>
+          Wait until the X11 server is accepting connections.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_for_text</literal>
+      </term>
+      <listitem>
+        <para>
+          Wait until the supplied regular expressions matches the
+          textual contents of the screen by using optical character
+          recognition (see <literal>get_screen_text</literal> and
+          <literal>get_screen_text_variants</literal>).
+        </para>
+        <note>
+          <para>
+            This requires passing <literal>enableOCR</literal> to the
+            test attribute set.
+          </para>
+        </note>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_for_console_text</literal>
+      </term>
+      <listitem>
+        <para>
+          Wait until the supplied regular expressions match a line of
+          the serial console output. This method is useful when OCR is
+          not possibile or accurate enough.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>wait_for_window</literal>
+      </term>
+      <listitem>
+        <para>
+          Wait until an X11 window has appeared whose name matches the
+          given regular expression, e.g.,
+          <literal>wait_for_window(&quot;Terminal&quot;)</literal>.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>copy_from_host</literal>
+      </term>
+      <listitem>
+        <para>
+          Copies a file from host to machine, e.g.,
+          <literal>copy_from_host(&quot;myfile&quot;, &quot;/etc/my/important/file&quot;)</literal>.
+        </para>
+        <para>
+          The first argument is the file on the host. The file needs to
+          be accessible while building the nix derivation. The second
+          argument is the location of the file on the machine.
+        </para>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>systemctl</literal>
+      </term>
+      <listitem>
+        <para>
+          Runs <literal>systemctl</literal> commands with optional
+          support for <literal>systemctl --user</literal>
+        </para>
+        <programlisting language="python">
+machine.systemctl(&quot;list-jobs --no-pager&quot;) # runs `systemctl list-jobs --no-pager`
+machine.systemctl(&quot;list-jobs --no-pager&quot;, &quot;any-user&quot;) # spawns a shell for `any-user` and runs `systemctl --user list-jobs --no-pager`
+</programlisting>
+      </listitem>
+    </varlistentry>
+    <varlistentry>
+      <term>
+        <literal>shell_interact</literal>
+      </term>
+      <listitem>
+        <para>
+          Allows you to directly interact with the guest shell. This
+          should only be used during test development, not in production
+          tests. Killing the interactive session with
+          <literal>Ctrl-d</literal> or <literal>Ctrl-c</literal> also
+          ends the guest session.
+        </para>
+      </listitem>
+    </varlistentry>
+  </variablelist>
+  <para>
+    To test user units declared by
+    <literal>systemd.user.services</literal> the optional
+    <literal>user</literal> argument can be used:
+  </para>
+  <programlisting language="python">
+machine.start()
+machine.wait_for_x()
+machine.wait_for_unit(&quot;xautolock.service&quot;, &quot;x-session-user&quot;)
+</programlisting>
+  <para>
+    This applies to <literal>systemctl</literal>,
+    <literal>get_unit_info</literal>, <literal>wait_for_unit</literal>,
+    <literal>start_job</literal> and <literal>stop_job</literal>.
+  </para>
+  <para>
+    For faster dev cycles it's also possible to disable the code-linters
+    (this shouldn't be commited though):
+  </para>
+  <programlisting language="bash">
+import ./make-test-python.nix {
+  skipLint = true;
+  machine =
+    { config, pkgs, ... }:
+    { configuration…
+    };
+
+  testScript =
+    ''
+      Python code…
+    '';
+}
+</programlisting>
+  <para>
+    This will produce a Nix warning at evaluation time. To fully disable
+    the linter, wrap the test script in comment directives to disable
+    the Black linter directly (again, don't commit this within the
+    Nixpkgs repository):
+  </para>
+  <programlisting language="bash">
+  testScript =
+    ''
+      # fmt: off
+      Python code…
+      # fmt: on
+    '';
+</programlisting>
+</section>
diff --git a/nixos/modules/config/shells-environment.nix b/nixos/modules/config/shells-environment.nix
index a0a20228a742..34e558d8603d 100644
--- a/nixos/modules/config/shells-environment.nix
+++ b/nixos/modules/config/shells-environment.nix
@@ -126,6 +126,14 @@ in
       type = types.bool;
     };
 
+    environment.localBinInPath = mkOption {
+      description = ''
+        Add ~/.local/bin/ to $PATH
+      '';
+      default = false;
+      type = types.bool;
+    };
+
     environment.binsh = mkOption {
       default = "${config.system.build.binsh}/bin/sh";
       defaultText = "\${config.system.build.binsh}/bin/sh";
@@ -198,6 +206,10 @@ in
           # ~/bin if it exists overrides other bin directories.
           export PATH="$HOME/bin:$PATH"
         ''}
+
+        ${optionalString cfg.localBinInPath ''
+          export PATH="$HOME/.local/bin:$PATH"
+        ''}
       '';
 
     system.activationScripts.binsh = stringAfter [ "stdio" ]
diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix
index afb81a2b56be..0a09a755e93c 100644
--- a/nixos/modules/security/systemd-confinement.nix
+++ b/nixos/modules/security/systemd-confinement.nix
@@ -105,7 +105,7 @@ in {
         wantsAPIVFS = lib.mkDefault (config.confinement.mode == "full-apivfs");
       in lib.mkIf config.confinement.enable {
         serviceConfig = {
-          RootDirectory = pkgs.runCommand rootName {} "mkdir \"$out\"";
+          RootDirectory = "/var/empty";
           TemporaryFileSystem = "/";
           PrivateMounts = lib.mkDefault true;
 
diff --git a/nixos/modules/services/audio/slimserver.nix b/nixos/modules/services/audio/slimserver.nix
index 8f94a2b49404..21632919699c 100644
--- a/nixos/modules/services/audio/slimserver.nix
+++ b/nixos/modules/services/audio/slimserver.nix
@@ -63,6 +63,7 @@ in {
         description = "Slimserver daemon user";
         home = cfg.dataDir;
         group = "slimserver";
+        isSystemUser = true;
       };
       groups.slimserver = {};
     };
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
index 0472fb4ba1e7..be44a43b6d3f 100644
--- a/nixos/modules/services/backup/sanoid.nix
+++ b/nixos/modules/services/backup/sanoid.nix
@@ -10,74 +10,51 @@ let
       description = "dataset/template options";
     };
 
-  # Default values from https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf
-
   commonOptions = {
     hourly = mkOption {
       description = "Number of hourly snapshots.";
-      type = types.ints.unsigned;
-      default = 48;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     daily = mkOption {
       description = "Number of daily snapshots.";
-      type = types.ints.unsigned;
-      default = 90;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     monthly = mkOption {
       description = "Number of monthly snapshots.";
-      type = types.ints.unsigned;
-      default = 6;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     yearly = mkOption {
       description = "Number of yearly snapshots.";
-      type = types.ints.unsigned;
-      default = 0;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     autoprune = mkOption {
       description = "Whether to automatically prune old snapshots.";
-      type = types.bool;
-      default = true;
+      type = with types; nullOr bool;
+      default = null;
     };
 
     autosnap = mkOption {
       description = "Whether to automatically take snapshots.";
-      type = types.bool;
-      default = true;
-    };
-
-    settings = mkOption {
-      description = ''
-        Free-form settings for this template/dataset. See
-        <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
-        for allowed values.
-      '';
-      type = datasetSettingsType;
-    };
-  };
-
-  commonConfig = config: {
-    settings = {
-      hourly = mkDefault config.hourly;
-      daily = mkDefault config.daily;
-      monthly = mkDefault config.monthly;
-      yearly = mkDefault config.yearly;
-      autoprune = mkDefault config.autoprune;
-      autosnap = mkDefault config.autosnap;
+      type = with types; nullOr bool;
+      default = null;
     };
   };
 
-  datasetOptions = {
-    useTemplate = mkOption {
+  datasetOptions = rec {
+    use_template = mkOption {
       description = "Names of the templates to use for this dataset.";
-      type = (types.listOf (types.enum (attrNames cfg.templates))) // {
-        description = "list of template names";
-      };
+      type = types.listOf (types.enum (attrNames cfg.templates));
       default = [];
     };
+    useTemplate = use_template;
 
     recursive = mkOption {
       description = "Whether to recursively snapshot dataset children.";
@@ -85,19 +62,12 @@ let
       default = false;
     };
 
-    processChildrenOnly = mkOption {
+    process_children_only = mkOption {
       description = "Whether to only snapshot child datasets if recursing.";
       type = types.bool;
       default = false;
     };
-  };
-
-  datasetConfig = config: {
-    settings = {
-      use_template = mkDefault config.useTemplate;
-      recursive = mkDefault config.recursive;
-      process_children_only = mkDefault config.processChildrenOnly;
-    };
+    processChildrenOnly = process_children_only;
   };
 
   # Extract pool names from configured datasets
@@ -109,11 +79,11 @@ let
       else generators.mkValueStringDefault {} v;
 
     mkKeyValue = k: v: if v == null then ""
+      else if k == "processChildrenOnly" then ""
+      else if k == "useTemplate" then ""
       else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
   in generators.toINI { inherit mkKeyValue; } cfg.settings;
 
-  configDir = pkgs.writeTextDir "sanoid.conf" configFile;
-
 in {
 
     # Interface
@@ -135,19 +105,21 @@ in {
       };
 
       datasets = mkOption {
-        type = types.attrsOf (types.submodule ({ config, ... }: {
+        type = types.attrsOf (types.submodule ({config, options, ...}: {
+          freeformType = datasetSettingsType;
           options = commonOptions // datasetOptions;
-          config = mkMerge [ (commonConfig config) (datasetConfig config) ];
+          config.use_template = mkAliasDefinitions (options.useTemplate or {});
+          config.process_children_only = mkAliasDefinitions (options.processChildrenOnly or {});
         }));
         default = {};
         description = "Datasets to snapshot.";
       };
 
       templates = mkOption {
-        type = types.attrsOf (types.submodule ({ config, ... }: {
+        type = types.attrsOf (types.submodule {
+          freeformType = datasetSettingsType;
           options = commonOptions;
-          config = commonConfig config;
-        }));
+        });
         default = {};
         description = "Templates for datasets.";
       };
@@ -177,8 +149,8 @@ in {
 
     config = mkIf cfg.enable {
       services.sanoid.settings = mkMerge [
-        (mapAttrs' (d: v: nameValuePair ("template_" + d) v.settings) cfg.templates)
-        (mapAttrs (d: v: v.settings) cfg.datasets)
+        (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates)
+        (mapAttrs (d: v: v) cfg.datasets)
       ];
 
       systemd.services.sanoid = {
@@ -191,7 +163,7 @@ in {
           ExecStart = lib.escapeShellArgs ([
             "${pkgs.sanoid}/bin/sanoid"
             "--cron"
-            "--configdir" configDir
+            "--configdir" (pkgs.writeTextDir "sanoid.conf" configFile)
           ] ++ cfg.extraArgs);
           ExecStopPost = map (pool: lib.escapeShellArgs [
             "+/run/booted-system/sw/bin/zfs" "unallow" "sanoid" pool
diff --git a/nixos/modules/services/misc/geoipupdate.nix b/nixos/modules/services/misc/geoipupdate.nix
index 5d87be928d98..3211d4d88e4d 100644
--- a/nixos/modules/services/misc/geoipupdate.nix
+++ b/nixos/modules/services/misc/geoipupdate.nix
@@ -99,9 +99,22 @@ in
       LockFile = "/run/geoipupdate/.lock";
     };
 
+    systemd.services.geoipupdate-create-db-dir = {
+      serviceConfig.Type = "oneshot";
+      script = ''
+        mkdir -p ${cfg.settings.DatabaseDirectory}
+        chmod 0755 ${cfg.settings.DatabaseDirectory}
+      '';
+    };
+
     systemd.services.geoipupdate = {
       description = "GeoIP Updater";
-      after = [ "network-online.target" "nss-lookup.target" ];
+      requires = [ "geoipupdate-create-db-dir.service" ];
+      after = [
+        "geoipupdate-create-db-dir.service"
+        "network-online.target"
+        "nss-lookup.target"
+      ];
       wants = [ "network-online.target" ];
       startAt = cfg.interval;
       serviceConfig = {
@@ -119,11 +132,9 @@ in
               };
             };
 
-            geoipupdateConf = pkgs.writeText "discourse.conf" (geoipupdateKeyValue cfg.settings);
+            geoipupdateConf = pkgs.writeText "geoipupdate.conf" (geoipupdateKeyValue cfg.settings);
 
             script = ''
-              mkdir -p "${cfg.settings.DatabaseDirectory}"
-              chmod 755 "${cfg.settings.DatabaseDirectory}"
               chown geoip "${cfg.settings.DatabaseDirectory}"
 
               cp ${geoipupdateConf} /run/geoipupdate/GeoIP.conf
@@ -139,7 +150,38 @@ in
         ReadWritePaths = cfg.settings.DatabaseDirectory;
         RuntimeDirectory = "geoipupdate";
         RuntimeDirectoryMode = 0700;
+        CapabilityBoundingSet = "";
+        PrivateDevices = true;
+        PrivateMounts = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        MemoryDenyWriteExecute = true;
+        LockPersonality = true;
+        SystemCallArchitectures = "native";
+      };
+    };
+
+    systemd.timers.geoipupdate-initial-run = {
+      wantedBy = [ "timers.target" ];
+      unitConfig.ConditionPathExists = "!${cfg.settings.DatabaseDirectory}";
+      timerConfig = {
+        Unit = "geoipupdate.service";
+        OnActiveSec = 0;
       };
     };
   };
+
+  meta.maintainers = [ lib.maintainers.talyz ];
 }
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
index 6d8dfcce933c..2748571be1f7 100644
--- a/nixos/modules/services/network-filesystems/ipfs.nix
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -231,9 +231,9 @@ in {
           }
         fi
       '' + optionalString cfg.autoMount ''
-        ipfs --local config Mounts.FuseAllowOther --json true
-        ipfs --local config Mounts.IPFS ${cfg.ipfsMountDir}
-        ipfs --local config Mounts.IPNS ${cfg.ipnsMountDir}
+        ipfs --offline config Mounts.FuseAllowOther --json true
+        ipfs --offline config Mounts.IPFS ${cfg.ipfsMountDir}
+        ipfs --offline config Mounts.IPNS ${cfg.ipnsMountDir}
       '' + concatStringsSep "\n" (collect
             isString
             (mapAttrsRecursive
@@ -243,7 +243,7 @@ in {
                 read value <<EOF
                 ${builtins.toJSON value}
                 EOF
-                ipfs --local config --json "${concatStringsSep "." path}" "$value"
+                ipfs --offline config --json "${concatStringsSep "." path}" "$value"
               '')
               ({ Addresses.API = cfg.apiAddress;
                  Addresses.Gateway = cfg.gatewayAddress;
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index 91caa2ccb422..2c96b94ca43c 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -453,6 +453,7 @@ in
               { ExecStart =
                   (optionalString cfg.startWhenNeeded "-") +
                   "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
+                  "-D " +  # don't detach into a daemon process
                   "-f /etc/ssh/sshd_config";
                 KillMode = "process";
               } // (if cfg.startWhenNeeded then {
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index c33a38179ee4..3f88ff53dff0 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -34,7 +34,7 @@ in {
     systemd.packages = [ cfg.package ];
     systemd.services.tailscaled = {
       wantedBy = [ "multi-user.target" ];
-      path = [ pkgs.openresolv ];
+      path = [ pkgs.openresolv pkgs.procps ];
       serviceConfig.Environment = [
         "PORT=${toString cfg.port}"
         ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName}"''
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
index 9c7166f381a3..d3ae072f86a8 100644
--- a/nixos/modules/services/web-apps/discourse.nix
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -30,6 +30,9 @@ in
       package = lib.mkOption {
         type = lib.types.package;
         default = pkgs.discourse;
+        apply = p: p.override {
+          plugins = lib.unique (p.enabledPlugins ++ cfg.plugins);
+        };
         defaultText = "pkgs.discourse";
         description = ''
           The discourse package to use.
@@ -731,8 +734,6 @@ in
 
           cp -r ${cfg.package}/share/discourse/config.dist/* /run/discourse/config/
           cp -r ${cfg.package}/share/discourse/public.dist/* /run/discourse/public/
-          cp -r ${cfg.package}/share/discourse/plugins.dist/* /run/discourse/plugins/
-          ${lib.concatMapStringsSep "\n" (p: "ln -sf ${p} /run/discourse/plugins/") cfg.plugins}
           ln -sf /var/lib/discourse/uploads /run/discourse/public/uploads
           ln -sf /var/lib/discourse/backups /run/discourse/public/backups
 
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
index 63e01dd054a5..7134b4321630 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
@@ -61,7 +61,7 @@ def write_loader_conf(profile: Optional[str], generation: int) -> None:
 
 
 def profile_path(profile: Optional[str], generation: int, name: str) -> str:
-    return os.readlink("%s/%s" % (system_dir(profile, generation), name))
+    return os.path.realpath("%s/%s" % (system_dir(profile, generation), name))
 
 
 def copy_from_profile(profile: Optional[str], generation: int, name: str, dry_run: bool = False) -> str:
diff --git a/nixos/tests/sanoid.nix b/nixos/tests/sanoid.nix
index c691bfc08ef7..1983945915fe 100644
--- a/nixos/tests/sanoid.nix
+++ b/nixos/tests/sanoid.nix
@@ -33,7 +33,7 @@ in {
 
           autosnap = true;
         };
-        datasets."pool/sanoid".useTemplate = [ "test" ];
+        datasets."pool/sanoid".use_template = [ "test" ];
         extraArgs = [ "--verbose" ];
       };
 
diff --git a/nixos/tests/vault.nix b/nixos/tests/vault.nix
index 59bccbe25959..c3b28b62695a 100644
--- a/nixos/tests/vault.nix
+++ b/nixos/tests/vault.nix
@@ -19,6 +19,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
       machine.wait_for_unit("vault.service")
       machine.wait_for_open_port(8200)
       machine.succeed("vault operator init")
-      machine.succeed("vault status | grep Sealed | grep true")
+      # vault now returns exit code 2 for sealed vaults
+      machine.fail("vault status")
+      machine.succeed("vault status || test $? -eq 2")
     '';
 })
diff --git a/nixos/tests/zsh-history.nix b/nixos/tests/zsh-history.nix
index 3109c3f65081..355687798406 100644
--- a/nixos/tests/zsh-history.nix
+++ b/nixos/tests/zsh-history.nix
@@ -23,7 +23,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     # Login
     default.wait_until_tty_matches(1, "login: ")
     default.send_chars("root\n")
-    default.wait_until_tty_matches(1, "root@default>")
+    default.wait_until_tty_matches(1, r"\nroot@default\b")
 
     # Generate some history
     default.send_chars("echo foobar\n")