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/default.nix2
-rw-r--r--nixos/doc/manual/development/option-declarations.section.md6
-rw-r--r--nixos/doc/manual/from_md/development/option-declarations.section.xml6
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2305.section.xml76
-rw-r--r--nixos/doc/manual/release-notes/rl-2305.section.md14
-rw-r--r--nixos/lib/make-options-doc/default.nix41
-rw-r--r--nixos/lib/make-options-doc/mergeJSON.py394
-rw-r--r--nixos/lib/make-options-doc/options-to-docbook.xsl43
-rw-r--r--nixos/lib/test-driver/default.nix2
-rw-r--r--nixos/lib/testing/testScript.nix2
-rw-r--r--nixos/modules/config/mysql.nix2
-rw-r--r--nixos/modules/i18n/input-method/kime.nix68
-rw-r--r--nixos/modules/installer/cd-dvd/channel.nix2
-rwxr-xr-xnixos/modules/installer/tools/nixos-install.sh26
-rw-r--r--nixos/modules/misc/documentation.nix8
-rw-r--r--nixos/modules/module-list.nix2
-rw-r--r--nixos/modules/security/acme/default.nix2
-rw-r--r--nixos/modules/security/audit.nix2
-rw-r--r--nixos/modules/security/systemd-confinement.nix1
-rw-r--r--nixos/modules/services/audio/mpd.nix2
-rw-r--r--nixos/modules/services/backup/restic.nix23
-rw-r--r--nixos/modules/services/mail/goeland.nix74
-rw-r--r--nixos/modules/services/misc/autosuspend.nix230
-rw-r--r--nixos/modules/services/misc/gpsd.nix2
-rw-r--r--nixos/modules/services/misc/weechat.nix2
-rw-r--r--nixos/modules/services/monitoring/grafana-image-renderer.nix2
-rw-r--r--nixos/modules/services/monitoring/tuptime.nix6
-rw-r--r--nixos/modules/services/monitoring/uptime-kuma.nix4
-rw-r--r--nixos/modules/services/networking/avahi-daemon.nix4
-rw-r--r--nixos/modules/services/networking/gnunet.nix4
-rw-r--r--nixos/modules/services/networking/nat.nix2
-rw-r--r--nixos/modules/services/networking/nomad.nix15
-rw-r--r--nixos/modules/services/networking/redsocks.nix3
-rw-r--r--nixos/modules/services/networking/rpcbind.nix2
-rw-r--r--nixos/modules/services/networking/ssh/lshd.nix2
-rw-r--r--nixos/modules/services/networking/tmate-ssh-server.nix2
-rw-r--r--nixos/modules/services/security/fail2ban.nix4
-rw-r--r--nixos/modules/services/torrent/magnetico.nix2
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix8
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix49
-rw-r--r--nixos/modules/system/boot/loader/init-script/init-script.nix2
-rwxr-xr-xnixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py50
-rw-r--r--nixos/modules/tasks/filesystems/envfs.nix2
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix4
-rw-r--r--nixos/tests/ceph-single-node.nix11
-rw-r--r--nixos/tests/installer.nix2
-rw-r--r--nixos/tests/nginx.nix2
-rw-r--r--nixos/tests/pantheon.nix11
-rw-r--r--nixos/tests/restic.nix45
-rw-r--r--nixos/tests/systemd-boot.nix4
50 files changed, 880 insertions, 394 deletions
diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix
index 9b72e840f4b10..a89e5e466500b 100644
--- a/nixos/doc/manual/default.nix
+++ b/nixos/doc/manual/default.nix
@@ -209,7 +209,7 @@ let
 in rec {
   inherit generatedSources;
 
-  inherit (optionsDoc) optionsJSON optionsNix optionsDocBook;
+  inherit (optionsDoc) optionsJSON optionsNix optionsDocBook optionsUsedDocbook;
 
   # Generate the NixOS manual.
   manualHTML = runCommand "nixos-manual-html"
diff --git a/nixos/doc/manual/development/option-declarations.section.md b/nixos/doc/manual/development/option-declarations.section.md
index aa747f47c9c81..18ec7ba903a9f 100644
--- a/nixos/doc/manual/development/option-declarations.section.md
+++ b/nixos/doc/manual/development/option-declarations.section.md
@@ -78,7 +78,7 @@ For example:
 
 ::: {#ex-options-declarations-util-mkEnableOption-magic .example}
 ```nix
-lib.mkEnableOption "magic"
+lib.mkEnableOption (lib.mdDoc "magic")
 # is like
 lib.mkOption {
   type = lib.types.bool;
@@ -113,7 +113,7 @@ Examples:
 
 ::: {#ex-options-declarations-util-mkPackageOption-hello .example}
 ```nix
-lib.mkPackageOption pkgs "hello" { }
+lib.mkPackageOptionMD pkgs "hello" { }
 # is like
 lib.mkOption {
   type = lib.types.package;
@@ -125,7 +125,7 @@ lib.mkOption {
 
 ::: {#ex-options-declarations-util-mkPackageOption-ghc .example}
 ```nix
-lib.mkPackageOption pkgs "GHC" {
+lib.mkPackageOptionMD pkgs "GHC" {
   default = [ "ghc" ];
   example = "pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])";
 }
diff --git a/nixos/doc/manual/from_md/development/option-declarations.section.xml b/nixos/doc/manual/from_md/development/option-declarations.section.xml
index cc4893939c280..af05e61363e4b 100644
--- a/nixos/doc/manual/from_md/development/option-declarations.section.xml
+++ b/nixos/doc/manual/from_md/development/option-declarations.section.xml
@@ -128,7 +128,7 @@ options = {
       </para>
       <anchor xml:id="ex-options-declarations-util-mkEnableOption-magic" />
       <programlisting language="nix">
-lib.mkEnableOption &quot;magic&quot;
+lib.mkEnableOption (lib.mdDoc &quot;magic&quot;)
 # is like
 lib.mkOption {
   type = lib.types.bool;
@@ -188,7 +188,7 @@ mkPackageOption pkgs &quot;name&quot; { default = [ &quot;path&quot; &quot;in&qu
         </para>
         <anchor xml:id="ex-options-declarations-util-mkPackageOption-hello" />
         <programlisting language="nix">
-lib.mkPackageOption pkgs &quot;hello&quot; { }
+lib.mkPackageOptionMD pkgs &quot;hello&quot; { }
 # is like
 lib.mkOption {
   type = lib.types.package;
@@ -199,7 +199,7 @@ lib.mkOption {
 </programlisting>
         <anchor xml:id="ex-options-declarations-util-mkPackageOption-ghc" />
         <programlisting language="nix">
-lib.mkPackageOption pkgs &quot;GHC&quot; {
+lib.mkPackageOptionMD pkgs &quot;GHC&quot; {
   default = [ &quot;ghc&quot; ];
   example = &quot;pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])&quot;;
 }
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
index 9a59fcb0a2c1c..1a9d3bdb6c6bf 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
@@ -85,6 +85,14 @@
       </listitem>
       <listitem>
         <para>
+          <link xlink:href="https://github.com/slurdge/goeland">goeland</link>,
+          an alternative to rss2email written in golang with many
+          filters. Available as
+          <link linkend="opt-services.goeland.enable">services.goeland</link>.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <link xlink:href="https://github.com/ellie/atuin">atuin</link>,
           a sync server for shell history. Available as
           <link linkend="opt-services.atuin.enable">services.atuin</link>.
@@ -122,6 +130,13 @@
           <link xlink:href="options.html#opt-services.photoprism.enable">services.photoprism</link>.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://github.com/languitar/autosuspend">autosuspend</link>,
+          a python daemon that suspends a system if certain conditions
+          are met, or not met.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-23.05-incompatibilities">
@@ -140,6 +155,30 @@
       </listitem>
       <listitem>
         <para>
+          <literal>checkInputs</literal> have been renamed to
+          <literal>nativeCheckInputs</literal>, because they behave the
+          same as <literal>nativeBuildInputs</literal> when
+          <literal>doCheck</literal> is set.
+          <literal>checkInputs</literal> now denote a new type of
+          dependencies, added to <literal>buildInputs</literal> when
+          <literal>doCheck</literal> is set. As a rule of thumb,
+          <literal>nativeCheckInputs</literal> are tools on
+          <literal>$PATH</literal> used during the tests, and
+          <literal>checkInputs</literal> are libraries which are linked
+          to executables built as part of the tests. Similarly,
+          <literal>installCheckInputs</literal> are renamed to
+          <literal>nativeInstallCheckInputs</literal>, corresponding to
+          <literal>nativeBuildInputs</literal>, and
+          <literal>installCheckInputs</literal> are a new type of
+          dependencies added to <literal>buildInputs</literal> when
+          <literal>doInstallCheck</literal> is set. (Note that this
+          change will not cause breakage to derivations with
+          <literal>strictDeps</literal> unset, which are most packages
+          except python, rust and go packages).
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>borgbackup</literal> module now has an option for
           inhibiting system sleep while backups are running, defaulting
           to off (not inhibiting sleep), available as
@@ -239,6 +278,17 @@
       </listitem>
       <listitem>
         <para>
+          Kime has been updated from 2.5.6 to 3.0.2 and the
+          <literal>i18n.inputMethod.kime.config</literal> option has
+          been removed. Users should use
+          <literal>daemonModules</literal>,
+          <literal>iconColor</literal>, and
+          <literal>extraConfig</literal> options under
+          <literal>i18n.inputMethod.kime</literal> instead.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           <literal>llvmPackages_rocm.llvm</literal> will not contain
           <literal>clang</literal> or <literal>compiler-rt</literal>.
           <literal>llvmPackages_rocm.clang</literal> will not contain
@@ -250,16 +300,6 @@
       </listitem>
       <listitem>
         <para>
-          The Nginx module now validates the syntax of config files at
-          build time. For more complex configurations (using
-          <literal>include</literal> with out-of-store files notably)
-          you may need to disable this check by setting
-          <link linkend="opt-services.nginx.validateConfig">services.nginx.validateConfig</link>
-          to <literal>false</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
           The EC2 image module previously detected and automatically
           mounted ext3-formatted instance store devices and partitions
           in stage-1 (initramfs), storing <literal>/tmp</literal> on the
@@ -386,6 +426,22 @@
       </listitem>
       <listitem>
         <para>
+          DocBook option documentation, which has been deprecated since
+          22.11, will now cause a warning when documentation is built.
+          Out-of-tree modules should migrate to using CommonMark
+          documentation as outlined in
+          <xref linkend="sec-option-declarations" /> to silence this
+          warning.
+        </para>
+        <para>
+          DocBook option documentation support will be removed in the
+          next release and CommonMark will become the default. DocBook
+          option documentation that has not been migrated until then
+          will no longer render properly or cause errors.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
           The <literal>dnsmasq</literal> service now takes configuration
           via the <literal>services.dnsmasq.settings</literal> attribute
           set. The option
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index d9edd64f42c1e..2563da719f62d 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -30,6 +30,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [stevenblack-blocklist](https://github.com/StevenBlack/hosts), A unified hosts file with base extensions for blocking unwanted websites. Available as [networking.stevenblack](options.html#opt-networking.stevenblack.enable).
 
+- [goeland](https://github.com/slurdge/goeland), an alternative to rss2email written in golang with many filters. Available as [services.goeland](#opt-services.goeland.enable).
+
 - [atuin](https://github.com/ellie/atuin), a sync server for shell history. Available as [services.atuin](#opt-services.atuin.enable).
 
 - [mmsd](https://gitlab.com/kop316/mmsd), a lower level daemon that transmits and recieves MMSes. Available as [services.mmsd](#opt-services.mmsd.enable).
@@ -40,12 +42,16 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [photoprism](https://photoprism.app/), a AI-Powered Photos App for the Decentralized Web. Available as [services.photoprism](options.html#opt-services.photoprism.enable).
 
+- [autosuspend](https://github.com/languitar/autosuspend), a python daemon that suspends a system if certain conditions are met, or not met.
+
 ## Backward Incompatibilities {#sec-release-23.05-incompatibilities}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
 - `carnix` and `cratesIO` has been removed due to being unmaintained, use alternatives such as [naersk](https://github.com/nix-community/naersk) and [crate2nix](https://github.com/kolloch/crate2nix) instead.
 
+- `checkInputs` have been renamed to `nativeCheckInputs`, because they behave the same as `nativeBuildInputs` when `doCheck` is set. `checkInputs` now denote a new type of dependencies, added to `buildInputs` when `doCheck` is set. As a rule of thumb, `nativeCheckInputs` are tools on `$PATH` used during the tests, and `checkInputs` are libraries which are linked to executables built as part of the tests. Similarly, `installCheckInputs` are renamed to `nativeInstallCheckInputs`, corresponding to `nativeBuildInputs`, and `installCheckInputs` are a new type of dependencies added to `buildInputs` when `doInstallCheck` is set. (Note that this change will not cause breakage to derivations with `strictDeps` unset, which are most packages except python, rust and go packages).
+
 - `borgbackup` module now has an option for inhibiting system sleep while backups are running, defaulting to off (not inhibiting sleep), available as [`services.borgbackup.jobs.<name>.inhibitsSleep`](#opt-services.borgbackup.jobs._name_.inhibitsSleep).
 
 - `podman` now uses the `netavark` network stack. Users will need to delete all of their local containers, images, volumes, etc, by running `podman system reset --force` once before upgrading their systems.
@@ -65,9 +71,9 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - The [services.unifi-video.openFirewall](#opt-services.unifi-video.openFirewall) module option default value has been changed from `true` to `false`. You will need to explicitly set this option to `true`, or configure your firewall.
 
-- `llvmPackages_rocm.llvm` will not contain `clang` or `compiler-rt`. `llvmPackages_rocm.clang` will not contain `llvm`. `llvmPackages_rocm.clangNoCompilerRt` has been removed in favor of using `llvmPackages_rocm.clang-unwrapped`.
+- Kime has been updated from 2.5.6 to 3.0.2 and the `i18n.inputMethod.kime.config` option has been removed. Users should use `daemonModules`, `iconColor`, and `extraConfig` options under `i18n.inputMethod.kime` instead.
 
-- The Nginx module now validates the syntax of config files at build time. For more complex configurations (using `include` with out-of-store files notably) you may need to disable this check by setting [services.nginx.validateConfig](#opt-services.nginx.validateConfig) to `false`.
+- `llvmPackages_rocm.llvm` will not contain `clang` or `compiler-rt`. `llvmPackages_rocm.clang` will not contain `llvm`. `llvmPackages_rocm.clangNoCompilerRt` has been removed in favor of using `llvmPackages_rocm.clang-unwrapped`.
 
 - The EC2 image module previously detected and automatically mounted ext3-formatted instance store devices and partitions in stage-1 (initramfs), storing `/tmp` on the first discovered device. This behaviour, which only catered to very specific use cases and could not be disabled, has been removed. Users relying on this should provide their own implementation, and probably use ext4 and perform the mount in stage-2.
 
@@ -97,6 +103,10 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - `services.mastodon` gained a tootctl wrapped named `mastodon-tootctl` similar to `nextcloud-occ` which can be executed from any user and switches to the configured mastodon user with sudo and sources the environment variables.
 
+- DocBook option documentation, which has been deprecated since 22.11, will now cause a warning when documentation is built. Out-of-tree modules should migrate to using CommonMark documentation as outlined in [](#sec-option-declarations) to silence this warning.
+
+  DocBook option documentation support will be removed in the next release and CommonMark will become the default. DocBook option documentation that has not been migrated until then will no longer render properly or cause errors.
+
 - The `dnsmasq` service now takes configuration via the
   `services.dnsmasq.settings` attribute set. The option
   `services.dnsmasq.extraConfig` will be deprecated when NixOS 22.11 reaches
diff --git a/nixos/lib/make-options-doc/default.nix b/nixos/lib/make-options-doc/default.nix
index e2ed7bb71885b..335217703c826 100644
--- a/nixos/lib/make-options-doc/default.nix
+++ b/nixos/lib/make-options-doc/default.nix
@@ -78,16 +78,13 @@ let
           title = args.title or null;
           name = args.name or (lib.concatStringsSep "." args.path);
         in ''
-          <listitem>
-            <para>
-              <link xlink:href="https://search.nixos.org/packages?show=${name}&amp;sort=relevance&amp;query=${name}">
-                <literal>${lib.optionalString (title != null) "${title} aka "}pkgs.${name}</literal>
-              </link>
-            </para>
-            ${lib.optionalString (args ? comment) "<para>${args.comment}</para>"}
-          </listitem>
+          - [`${lib.optionalString (title != null) "${title} aka "}pkgs.${name}`](
+              https://search.nixos.org/packages?show=${name}&sort=relevance&query=${name}
+            )${
+              lib.optionalString (args ? comment) "\n\n  ${args.comment}"
+            }
         '';
-    in "<itemizedlist>${lib.concatStringsSep "\n" (map (p: describe (unpack p)) packages)}</itemizedlist>";
+    in lib.concatMapStrings (p: describe (unpack p)) packages;
 
   optionsNix = builtins.listToAttrs (map (o: { name = o.name; value = removeAttrs o ["name" "visible" "internal"]; }) optionsList);
 
@@ -121,7 +118,20 @@ in rec {
             inherit self;
             includeSiteCustomize = true;
            });
-         in self.withPackages (p: [ p.mistune ]))
+         in self.withPackages (p:
+           let
+            # TODO add our own small test suite when rendering is split out into a new tool
+            markdown-it-py = p.markdown-it-py.override {
+              disableTests = true;
+            };
+            mdit-py-plugins = p.mdit-py-plugins.override {
+              inherit markdown-it-py;
+              disableTests = true;
+            };
+          in [
+            markdown-it-py
+            mdit-py-plugins
+          ]))
       ];
       options = builtins.toFile "options.json"
         (builtins.unsafeDiscardStringContext (builtins.toJSON optionsNix));
@@ -139,9 +149,10 @@ in rec {
       dst=$out/share/doc/nixos
       mkdir -p $dst
 
+      TOUCH_IF_DB=$dst/.used-docbook \
       python ${./mergeJSON.py} \
         ${lib.optionalString warningsAreErrors "--warnings-are-errors"} \
-        ${lib.optionalString (! allowDocBook) "--error-on-docbook"} \
+        ${if allowDocBook then "--warn-on-docbook" else "--error-on-docbook"} \
         ${lib.optionalString markdownByDefault "--markdown-by-default"} \
         $baseJSON $options \
         > $dst/options.json
@@ -153,6 +164,14 @@ in rec {
       echo "file json-br $dst/options.json.br" >> $out/nix-support/hydra-build-products
     '';
 
+  optionsUsedDocbook = pkgs.runCommand "options-used-docbook" {} ''
+    if [ -e ${optionsJSON}/share/doc/nixos/.used-docbook ]; then
+      echo 1
+    else
+      echo 0
+    fi >"$out"
+  '';
+
   # Convert options.json into an XML file.
   # The actual generation of the xml file is done in nix purely for the convenience
   # of not having to generate the xml some other way
diff --git a/nixos/lib/make-options-doc/mergeJSON.py b/nixos/lib/make-options-doc/mergeJSON.py
index c4f490fc2ad87..2de0bbae1d913 100644
--- a/nixos/lib/make-options-doc/mergeJSON.py
+++ b/nixos/lib/make-options-doc/mergeJSON.py
@@ -3,9 +3,17 @@ import json
 import os
 import sys
 from typing import Any, Dict, List
+from collections.abc import MutableMapping, Sequence
+import inspect
 
 # for MD conversion
-import mistune
+import markdown_it
+import markdown_it.renderer
+from markdown_it.token import Token
+from markdown_it.utils import OptionsDict
+from mdit_py_plugins.container import container_plugin
+from mdit_py_plugins.deflist import deflist_plugin
+from mdit_py_plugins.myst_role import myst_role_plugin
 import re
 from xml.sax.saxutils import escape, quoteattr
 
@@ -49,155 +57,175 @@ def unpivot(options: Dict[Key, Option]) -> Dict[str, JSON]:
 
 manpage_urls = json.load(open(os.getenv('MANPAGE_URLS')))
 
-admonitions = {
-    '.warning': 'warning',
-    '.important': 'important',
-    '.note': 'note'
-}
-class Renderer(mistune.renderers.BaseRenderer):
-    def _get_method(self, name):
-        try:
-            return super(Renderer, self)._get_method(name)
-        except AttributeError:
-            def not_supported(*args, **kwargs):
-                raise NotImplementedError("md node not supported yet", name, args, **kwargs)
-            return not_supported
-
-    def text(self, text):
-        return escape(text)
-    def paragraph(self, text):
-        return text + "\n\n"
-    def newline(self):
-        return "<literallayout>\n</literallayout>"
-    def codespan(self, text):
-        return f"<literal>{escape(text)}</literal>"
-    def block_code(self, text, info=None):
-        info = f" language={quoteattr(info)}" if info is not None else ""
-        return f"<programlisting{info}>\n{escape(text)}</programlisting>"
-    def link(self, link, text=None, title=None):
-        tag = "link"
-        if link[0:1] == '#':
-            if text == "":
-                tag = "xref"
-            attr = "linkend"
-            link = quoteattr(link[1:])
-        else:
-            # try to faithfully reproduce links that were of the form <link href="..."/>
-            # in docbook format
-            if text == link:
-                text = ""
-            attr = "xlink:href"
-            link = quoteattr(link)
-        return f"<{tag} {attr}={link}>{text}</{tag}>"
-    def list(self, text, ordered, level, start=None):
-        if ordered:
-            raise NotImplementedError("ordered lists not supported yet")
-        return f"<itemizedlist>\n{text}\n</itemizedlist>"
-    def list_item(self, text, level):
-        return f"<listitem><para>{text}</para></listitem>\n"
-    def block_text(self, text):
-        return text
-    def emphasis(self, text):
-        return f"<emphasis>{text}</emphasis>"
-    def strong(self, text):
-        return f"<emphasis role=\"strong\">{text}</emphasis>"
-    def admonition(self, text, kind):
-        if kind not in admonitions:
-            raise NotImplementedError(f"admonition {kind} not supported yet")
-        tag = admonitions[kind]
-        # we don't keep whitespace here because usually we'll contain only
-        # a single paragraph and the original docbook string is no longer
-        # available to restore the trailer.
-        return f"<{tag}><para>{text.rstrip()}</para></{tag}>"
-    def block_quote(self, text):
-        return f"<blockquote><para>{text}</para></blockquote>"
-    def command(self, text):
-        return f"<command>{escape(text)}</command>"
-    def option(self, text):
-        return f"<option>{escape(text)}</option>"
-    def file(self, text):
-        return f"<filename>{escape(text)}</filename>"
-    def var(self, text):
-        return f"<varname>{escape(text)}</varname>"
-    def env(self, text):
-        return f"<envar>{escape(text)}</envar>"
-    def manpage(self, page, section):
-        man = f"{page}({section})"
-        title = f"<refentrytitle>{escape(page)}</refentrytitle>"
-        vol = f"<manvolnum>{escape(section)}</manvolnum>"
-        ref = f"<citerefentry>{title}{vol}</citerefentry>"
-        if man in manpage_urls:
-            return self.link(manpage_urls[man], text=ref)
-        else:
-            return ref
-
-    def finalize(self, data):
-        return "".join(data)
-
-def p_command(md):
-    COMMAND_PATTERN = r'\{command\}`(.*?)`'
-    def parse(self, m, state):
-        return ('command', m.group(1))
-    md.inline.register_rule('command', COMMAND_PATTERN, parse)
-    md.inline.rules.append('command')
-
-def p_file(md):
-    FILE_PATTERN = r'\{file\}`(.*?)`'
-    def parse(self, m, state):
-        return ('file', m.group(1))
-    md.inline.register_rule('file', FILE_PATTERN, parse)
-    md.inline.rules.append('file')
-
-def p_var(md):
-    VAR_PATTERN = r'\{var\}`(.*?)`'
-    def parse(self, m, state):
-        return ('var', m.group(1))
-    md.inline.register_rule('var', VAR_PATTERN, parse)
-    md.inline.rules.append('var')
-
-def p_env(md):
-    ENV_PATTERN = r'\{env\}`(.*?)`'
-    def parse(self, m, state):
-        return ('env', m.group(1))
-    md.inline.register_rule('env', ENV_PATTERN, parse)
-    md.inline.rules.append('env')
-
-def p_option(md):
-    OPTION_PATTERN = r'\{option\}`(.*?)`'
-    def parse(self, m, state):
-        return ('option', m.group(1))
-    md.inline.register_rule('option', OPTION_PATTERN, parse)
-    md.inline.rules.append('option')
-
-def p_manpage(md):
-    MANPAGE_PATTERN = r'\{manpage\}`(.*?)\((.+?)\)`'
-    def parse(self, m, state):
-        return ('manpage', m.group(1), m.group(2))
-    md.inline.register_rule('manpage', MANPAGE_PATTERN, parse)
-    md.inline.rules.append('manpage')
-
-def p_admonition(md):
-    ADMONITION_PATTERN = re.compile(r'^::: \{([^\n]*?)\}\n(.*?)^:::$\n*', flags=re.MULTILINE|re.DOTALL)
-    def parse(self, m, state):
-        return {
-            'type': 'admonition',
-            'children': self.parse(m.group(2), state),
-            'params': [ m.group(1) ],
+class Renderer(markdown_it.renderer.RendererProtocol):
+    __output__ = "docbook"
+    def __init__(self, parser=None):
+        self.rules = {
+            k: v
+            for k, v in inspect.getmembers(self, predicate=inspect.ismethod)
+            if not (k.startswith("render") or k.startswith("_"))
+        } | {
+            "container_{.note}_open": self._note_open,
+            "container_{.note}_close": self._note_close,
+            "container_{.important}_open": self._important_open,
+            "container_{.important}_close": self._important_close,
+            "container_{.warning}_open": self._warning_open,
+            "container_{.warning}_close": self._warning_close,
         }
-    md.block.register_rule('admonition', ADMONITION_PATTERN, parse)
-    md.block.rules.append('admonition')
-
-md = mistune.create_markdown(renderer=Renderer(), plugins=[
-    p_command, p_file, p_var, p_env, p_option, p_manpage, p_admonition
-])
+    def render(self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping) -> str:
+        assert '-link-tag-stack' not in env
+        env['-link-tag-stack'] = []
+        assert '-deflist-stack' not in env
+        env['-deflist-stack'] = []
+        def do_one(i, token):
+            if token.type == "inline":
+                assert token.children is not None
+                return self.renderInline(token.children, options, env)
+            elif token.type in self.rules:
+                return self.rules[token.type](tokens[i], tokens, i, options, env)
+            else:
+                raise NotImplementedError("md token not supported yet", token)
+        return "".join(map(lambda arg: do_one(*arg), enumerate(tokens)))
+    def renderInline(self, tokens: Sequence[Token], options: OptionsDict, env: MutableMapping) -> str:
+        # HACK to support docbook links and xrefs. link handling is only necessary because the docbook
+        # manpage stylesheet converts - in urls to a mathematical minus, which may be somewhat incorrect.
+        for i, token in enumerate(tokens):
+            if token.type != 'link_open':
+                continue
+            token.tag = 'link'
+            # turn [](#foo) into xrefs
+            if token.attrs['href'][0:1] == '#' and tokens[i + 1].type == 'link_close':
+                token.tag = "xref"
+            # turn <x> into links without contents
+            if tokens[i + 1].type == 'text' and tokens[i + 1].content == token.attrs['href']:
+                tokens[i + 1].content = ''
+
+        def do_one(i, token):
+            if token.type in self.rules:
+                return self.rules[token.type](tokens[i], tokens, i, options, env)
+            else:
+                raise NotImplementedError("md node not supported yet", token)
+        return "".join(map(lambda arg: do_one(*arg), enumerate(tokens)))
+
+    def text(self, token, tokens, i, options, env):
+        return escape(token.content)
+    def paragraph_open(self, token, tokens, i, options, env):
+        return "<para>"
+    def paragraph_close(self, token, tokens, i, options, env):
+        return "</para>"
+    def hardbreak(self, token, tokens, i, options, env):
+        return "<literallayout>\n</literallayout>"
+    def softbreak(self, token, tokens, i, options, env):
+        # should check options.breaks() and emit hard break if so
+        return "\n"
+    def code_inline(self, token, tokens, i, options, env):
+        return f"<literal>{escape(token.content)}</literal>"
+    def code_block(self, token, tokens, i, options, env):
+        return f"<programlisting>{escape(token.content)}</programlisting>"
+    def link_open(self, token, tokens, i, options, env):
+        env['-link-tag-stack'].append(token.tag)
+        (attr, start) = ('linkend', 1) if token.attrs['href'][0] == '#' else ('xlink:href', 0)
+        return f"<{token.tag} {attr}={quoteattr(token.attrs['href'][start:])}>"
+    def link_close(self, token, tokens, i, options, env):
+        return f"</{env['-link-tag-stack'].pop()}>"
+    def list_item_open(self, token, tokens, i, options, env):
+        return "<listitem>"
+    def list_item_close(self, token, tokens, i, options, env):
+        return "</listitem>\n"
+    # HACK open and close para for docbook change size. remove soon.
+    def bullet_list_open(self, token, tokens, i, options, env):
+        return "<para><itemizedlist>\n"
+    def bullet_list_close(self, token, tokens, i, options, env):
+        return "\n</itemizedlist></para>"
+    def em_open(self, token, tokens, i, options, env):
+        return "<emphasis>"
+    def em_close(self, token, tokens, i, options, env):
+        return "</emphasis>"
+    def strong_open(self, token, tokens, i, options, env):
+        return "<emphasis role=\"strong\">"
+    def strong_close(self, token, tokens, i, options, env):
+        return "</emphasis>"
+    def fence(self, token, tokens, i, options, env):
+        info = f" language={quoteattr(token.info)}" if token.info != "" else ""
+        return f"<programlisting{info}>{escape(token.content)}</programlisting>"
+    def blockquote_open(self, token, tokens, i, options, env):
+        return "<para><blockquote>"
+    def blockquote_close(self, token, tokens, i, options, env):
+        return "</blockquote></para>"
+    def _note_open(self, token, tokens, i, options, env):
+        return "<para><note>"
+    def _note_close(self, token, tokens, i, options, env):
+        return "</note></para>"
+    def _important_open(self, token, tokens, i, options, env):
+        return "<para><important>"
+    def _important_close(self, token, tokens, i, options, env):
+        return "</important></para>"
+    def _warning_open(self, token, tokens, i, options, env):
+        return "<para><warning>"
+    def _warning_close(self, token, tokens, i, options, env):
+        return "</warning></para>"
+    # markdown-it emits tokens based on the html syntax tree, but docbook is
+    # slightly different. html has <dl>{<dt/>{<dd/>}}</dl>,
+    # docbook has <variablelist>{<varlistentry><term/><listitem/></varlistentry>}<variablelist>
+    # we have to reject multiple definitions for the same term for time being.
+    def dl_open(self, token, tokens, i, options, env):
+        env['-deflist-stack'].append({})
+        return "<para><variablelist>"
+    def dl_close(self, token, tokens, i, options, env):
+        env['-deflist-stack'].pop()
+        return "</variablelist></para>"
+    def dt_open(self, token, tokens, i, options, env):
+        env['-deflist-stack'][-1]['has-dd'] = False
+        return "<varlistentry><term>"
+    def dt_close(self, token, tokens, i, options, env):
+        return "</term>"
+    def dd_open(self, token, tokens, i, options, env):
+        if env['-deflist-stack'][-1]['has-dd']:
+            raise Exception("multiple definitions per term not supported")
+        env['-deflist-stack'][-1]['has-dd'] = True
+        return "<listitem>"
+    def dd_close(self, token, tokens, i, options, env):
+        return "</listitem></varlistentry>"
+    def myst_role(self, token, tokens, i, options, env):
+        if token.meta['name'] == 'command':
+            return f"<command>{escape(token.content)}</command>"
+        if token.meta['name'] == 'file':
+            return f"<filename>{escape(token.content)}</filename>"
+        if token.meta['name'] == 'var':
+            return f"<varname>{escape(token.content)}</varname>"
+        if token.meta['name'] == 'env':
+            return f"<envar>{escape(token.content)}</envar>"
+        if token.meta['name'] == 'option':
+            return f"<option>{escape(token.content)}</option>"
+        if token.meta['name'] == 'manpage':
+            [page, section] = [ s.strip() for s in token.content.rsplit('(', 1) ]
+            section = section[:-1]
+            man = f"{page}({section})"
+            title = f"<refentrytitle>{escape(page)}</refentrytitle>"
+            vol = f"<manvolnum>{escape(section)}</manvolnum>"
+            ref = f"<citerefentry>{title}{vol}</citerefentry>"
+            if man in manpage_urls:
+                return f"<link xlink:href={quoteattr(manpage_urls[man])}>{ref}</link>"
+            else:
+                return ref
+        raise NotImplementedError("md node not supported yet", token)
+
+md = (
+    markdown_it.MarkdownIt(renderer_cls=Renderer)
+    # TODO maybe fork the plugin and have only a single rule for all?
+    .use(container_plugin, name="{.note}")
+    .use(container_plugin, name="{.important}")
+    .use(container_plugin, name="{.warning}")
+    .use(deflist_plugin)
+    .use(myst_role_plugin)
+)
 
 # converts in-place!
 def convertMD(options: Dict[str, Any]) -> str:
     def convertString(path: str, text: str) -> str:
         try:
-            rendered = md(text)
-            # keep trailing spaces so we can diff the generated XML to check for conversion bugs.
-            return rendered.rstrip() + text[len(text.rstrip()):]
+            rendered = md.render(text)
+            return rendered
         except:
             print(f"error in {path}")
             raise
@@ -208,19 +236,45 @@ def convertMD(options: Dict[str, Any]) -> str:
         if '_type' not in option[key]: return False
         return option[key]['_type'] == typ
 
+    def convertCode(name: str, option: Dict[str, Any], key: str):
+        rendered = f"{key}-db"
+        if optionIs(option, key, 'literalMD'):
+            option[rendered] = convertString(name, f"*{key.capitalize()}:*\n{option[key]['text']}")
+        elif optionIs(option, key, 'literalExpression'):
+            code = option[key]['text']
+            # for multi-line code blocks we only have to count ` runs at the beginning
+            # of a line, but this is much easier.
+            multiline = '\n' in code
+            longest, current = (0, 0)
+            for c in code:
+                current = current + 1 if c == '`' else 0
+                longest = max(current, longest)
+            # inline literals need a space to separate ticks from content, code blocks
+            # need newlines. inline literals need one extra tick, code blocks need three.
+            ticks, sep = ('`' * (longest + (3 if multiline else 1)), '\n' if multiline else ' ')
+            code = f"{ticks}{sep}{code}{sep}{ticks}"
+            option[rendered] = convertString(name, f"*{key.capitalize()}:*\n{code}")
+        elif optionIs(option, key, 'literalDocBook'):
+            option[rendered] = f"<para><emphasis>{key.capitalize()}:</emphasis> {option[key]['text']}</para>"
+        elif key in option:
+            raise Exception(f"{name} {key} has unrecognized type", option[key])
+
     for (name, option) in options.items():
         try:
             if optionIs(option, 'description', 'mdDoc'):
                 option['description'] = convertString(name, option['description']['text'])
             elif markdownByDefault:
                 option['description'] = convertString(name, option['description'])
+            else:
+                option['description'] = ("<nixos:option-description><para>" +
+                                         option['description'] +
+                                         "</para></nixos:option-description>")
 
-            if optionIs(option, 'example', 'literalMD'):
-                docbook = convertString(name, option['example']['text'])
-                option['example'] = { '_type': 'literalDocBook', 'text': docbook }
-            if optionIs(option, 'default', 'literalMD'):
-                docbook = convertString(name, option['default']['text'])
-                option['default'] = { '_type': 'literalDocBook', 'text': docbook }
+            convertCode(name, option, 'example')
+            convertCode(name, option, 'default')
+
+            if 'relatedPackages' in option:
+                option['relatedPackages'] = convertString(name, option['relatedPackages'])
         except Exception as e:
             raise Exception(f"Failed to render option {name}: {str(e)}")
 
@@ -228,6 +282,7 @@ def convertMD(options: Dict[str, Any]) -> str:
     return options
 
 warningsAreErrors = False
+warnOnDocbook = False
 errorOnDocbook = False
 markdownByDefault = False
 optOffset = 0
@@ -235,7 +290,10 @@ for arg in sys.argv[1:]:
     if arg == "--warnings-are-errors":
         optOffset += 1
         warningsAreErrors = True
-    if arg == "--error-on-docbook":
+    if arg == "--warn-on-docbook":
+        optOffset += 1
+        warnOnDocbook = True
+    elif arg == "--error-on-docbook":
         optOffset += 1
         errorOnDocbook = True
     if arg == "--markdown-by-default":
@@ -278,26 +336,27 @@ def is_docbook(o, key):
 # check that every option has a description
 hasWarnings = False
 hasErrors = False
-hasDocBookErrors = False
+hasDocBook = False
 for (k, v) in options.items():
-    if errorOnDocbook:
+    if warnOnDocbook or errorOnDocbook:
+        kind = "error" if errorOnDocbook else "warning"
         if isinstance(v.value.get('description', {}), str):
-            hasErrors = True
-            hasDocBookErrors = True
+            hasErrors |= errorOnDocbook
+            hasDocBook = True
             print(
-                f"\x1b[1;31merror: option {v.name} description uses DocBook\x1b[0m",
+                f"\x1b[1;31m{kind}: option {v.name} description uses DocBook\x1b[0m",
                 file=sys.stderr)
         elif is_docbook(v.value, 'defaultText'):
-            hasErrors = True
-            hasDocBookErrors = True
+            hasErrors |= errorOnDocbook
+            hasDocBook = True
             print(
-                f"\x1b[1;31merror: option {v.name} default uses DocBook\x1b[0m",
+                f"\x1b[1;31m{kind}: option {v.name} default uses DocBook\x1b[0m",
                 file=sys.stderr)
         elif is_docbook(v.value, 'example'):
-            hasErrors = True
-            hasDocBookErrors = True
+            hasErrors |= errorOnDocbook
+            hasDocBook = True
             print(
-                f"\x1b[1;31merror: option {v.name} example uses DocBook\x1b[0m",
+                f"\x1b[1;31m{kind}: option {v.name} example uses DocBook\x1b[0m",
                 file=sys.stderr)
 
     if v.value.get('description', None) is None:
@@ -310,10 +369,14 @@ for (k, v) in options.items():
             f"\x1b[1;31m{severity}: option {v.name} has no type. Please specify a valid type, see " +
             "https://nixos.org/manual/nixos/stable/index.html#sec-option-types\x1b[0m", file=sys.stderr)
 
-if hasDocBookErrors:
+if hasDocBook:
+    (why, what) = (
+        ("disallowed for in-tree modules", "contribution") if errorOnDocbook
+        else ("deprecated for option documentation", "module")
+    )
     print("Explanation: The documentation contains descriptions, examples, or defaults written in DocBook. " +
         "NixOS is in the process of migrating from DocBook to Markdown, and " +
-        "DocBook is disallowed for in-tree modules. To change your contribution to "+
+        f"DocBook is {why}. To change your {what} to "+
         "use Markdown, apply mdDoc and literalMD and use the *MD variants of option creation " +
         "functions where they are available. For example:\n" +
         "\n" +
@@ -326,6 +389,9 @@ if hasDocBookErrors:
         "  example.package = mkPackageOptionMD pkgs \"your-package\" {};\n" +
         "  imports = [ (mkAliasOptionModuleMD [ \"example\" \"args\" ] [ \"example\" \"settings\" ]) ];",
         file = sys.stderr)
+    with open(os.getenv('TOUCH_IF_DB'), 'x'):
+        # just make sure it exists
+        pass
 
 if hasErrors:
     sys.exit(1)
diff --git a/nixos/lib/make-options-doc/options-to-docbook.xsl b/nixos/lib/make-options-doc/options-to-docbook.xsl
index ac49659c681f8..a2e88febdaff3 100644
--- a/nixos/lib/make-options-doc/options-to-docbook.xsl
+++ b/nixos/lib/make-options-doc/options-to-docbook.xsl
@@ -53,12 +53,8 @@
 
             <listitem>
 
-              <nixos:option-description>
-                <para>
-                  <xsl:value-of disable-output-escaping="yes"
-                                select="attr[@name = 'description']/string/@value" />
-                </para>
-              </nixos:option-description>
+              <xsl:value-of disable-output-escaping="yes"
+                            select="attr[@name = 'description']/string/@value" />
 
               <xsl:if test="attr[@name = 'type']">
                 <para>
@@ -72,29 +68,22 @@
                 </para>
               </xsl:if>
 
-              <xsl:if test="attr[@name = 'default']">
-                <para>
-                  <emphasis>Default:</emphasis>
-                  <xsl:text> </xsl:text>
-                  <xsl:apply-templates select="attr[@name = 'default']/*" mode="top" />
-                </para>
+              <xsl:if test="attr[@name = 'default-db']">
+                <xsl:value-of disable-output-escaping="yes"
+                              select="attr[@name = 'default-db']/string/@value" />
               </xsl:if>
 
-              <xsl:if test="attr[@name = 'example']">
-                <para>
-                  <emphasis>Example:</emphasis>
-                  <xsl:text> </xsl:text>
-                  <xsl:apply-templates select="attr[@name = 'example']/*" mode="top" />
-                </para>
+              <xsl:if test="attr[@name = 'example-db']">
+                <xsl:value-of disable-output-escaping="yes"
+                              select="attr[@name = 'example-db']/string/@value" />
               </xsl:if>
 
               <xsl:if test="attr[@name = 'relatedPackages']">
                 <para>
                   <emphasis>Related packages:</emphasis>
-                  <xsl:text> </xsl:text>
-                  <xsl:value-of disable-output-escaping="yes"
-                                select="attr[@name = 'relatedPackages']/string/@value" />
                 </para>
+                <xsl:value-of disable-output-escaping="yes"
+                              select="attr[@name = 'relatedPackages']/string/@value" />
               </xsl:if>
 
               <xsl:if test="count(attr[@name = 'declarations']/list/*) != 0">
@@ -121,18 +110,6 @@
   </xsl:template>
 
 
-  <xsl:template match="attrs[attr[@name = '_type' and string[@value = 'literalExpression']]]" mode = "top">
-    <xsl:choose>
-      <xsl:when test="contains(attr[@name = 'text']/string/@value, '&#010;')">
-        <programlisting><xsl:value-of select="attr[@name = 'text']/string/@value" /></programlisting>
-      </xsl:when>
-      <xsl:otherwise>
-        <literal><xsl:value-of select="attr[@name = 'text']/string/@value" /></literal>
-      </xsl:otherwise>
-    </xsl:choose>
-  </xsl:template>
-
-
   <xsl:template match="attrs[attr[@name = '_type' and string[@value = 'literalDocBook']]]" mode = "top">
     <xsl:value-of disable-output-escaping="yes" select="attr[@name = 'text']/string/@value" />
   </xsl:template>
diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix
index e3786622c3c58..33313059fff77 100644
--- a/nixos/lib/test-driver/default.nix
+++ b/nixos/lib/test-driver/default.nix
@@ -31,7 +31,7 @@ python3Packages.buildPythonApplication rec {
     ++ extraPythonPackages python3Packages;
 
   doCheck = true;
-  checkInputs = with python3Packages; [ mypy pylint black ];
+  nativeCheckInputs = with python3Packages; [ mypy pylint black ];
   checkPhase = ''
     mypy --disallow-untyped-defs \
           --no-implicit-optional \
diff --git a/nixos/lib/testing/testScript.nix b/nixos/lib/testing/testScript.nix
index 5d4181c5f5dd5..5c36d754d79d7 100644
--- a/nixos/lib/testing/testScript.nix
+++ b/nixos/lib/testing/testScript.nix
@@ -7,7 +7,7 @@ in
   options = {
     testScript = mkOption {
       type = either str (functionTo str);
-      description = ''
+      description = mdDoc ''
         A series of python declarations and statements that you write to perform
         the test.
       '';
diff --git a/nixos/modules/config/mysql.nix b/nixos/modules/config/mysql.nix
index af20a5e953567..2f13c56f2ae59 100644
--- a/nixos/modules/config/mysql.nix
+++ b/nixos/modules/config/mysql.nix
@@ -181,7 +181,7 @@ in
                 example = "pid";
                 description = lib.mdDoc ''
                   The name of the column in the log table to which the pid of the
-                  process utilising the `pam_mysql's` authentication
+                  process utilising the `pam_mysql` authentication
                   service is stored.
                 '';
               };
diff --git a/nixos/modules/i18n/input-method/kime.nix b/nixos/modules/i18n/input-method/kime.nix
index 29224a6bf75fb..e82996926b28b 100644
--- a/nixos/modules/i18n/input-method/kime.nix
+++ b/nixos/modules/i18n/input-method/kime.nix
@@ -1,40 +1,37 @@
 { config, pkgs, lib, generators, ... }:
-with lib;
-let
-  cfg = config.i18n.inputMethod.kime;
-  yamlFormat = pkgs.formats.yaml { };
-in
-{
-  options = {
-    i18n.inputMethod.kime = {
-      config = mkOption {
-        type = yamlFormat.type;
-        default = { };
-        example = literalExpression ''
-          {
-            daemon = {
-              modules = ["Xim" "Indicator"];
-            };
+let imcfg = config.i18n.inputMethod;
+in {
+  imports = [
+    (lib.mkRemovedOptionModule [ "i18n" "inputMethod" "kime" "config" ] "Use i18n.inputMethod.kime.* instead")
+  ];
 
-            indicator = {
-              icon_color = "White";
-            };
-
-            engine = {
-              hangul = {
-                layout = "dubeolsik";
-              };
-            };
-          }
-          '';
-        description = lib.mdDoc ''
-          kime configuration. Refer to <https://github.com/Riey/kime/blob/v${pkgs.kime.version}/docs/CONFIGURATION.md> for details on supported values.
-        '';
-      };
+  options.i18n.inputMethod.kime = {
+    daemonModules = lib.mkOption {
+      type = lib.types.listOf (lib.types.enum [ "Xim" "Wayland" "Indicator" ]);
+      default = [ "Xim" "Wayland" "Indicator" ];
+      example = [ "Xim" "Indicator" ];
+      description = lib.mdDoc ''
+        List of enabled daemon modules
+      '';
+    };
+    iconColor = lib.mkOption {
+      type = lib.types.enum [ "Black" "White" ];
+      default = "Black";
+      example = "White";
+      description = lib.mdDoc ''
+        Color of the indicator icon
+      '';
+    };
+    extraConfig = lib.mkOption {
+      type = lib.types.lines;
+      default = "";
+      description = lib.mdDoc ''
+        extra kime configuration. Refer to <https://github.com/Riey/kime/blob/v${pkgs.kime.version}/docs/CONFIGURATION.md> for details on supported values.
+      '';
     };
   };
 
-  config = mkIf (config.i18n.inputMethod.enabled == "kime") {
+  config = lib.mkIf (imcfg.enabled == "kime") {
     i18n.inputMethod.package = pkgs.kime;
 
     environment.variables = {
@@ -43,7 +40,12 @@ in
       XMODIFIERS    = "@im=kime";
     };
 
-    environment.etc."xdg/kime/config.yaml".text = replaceStrings [ "\\\\" ] [ "\\" ] (builtins.toJSON cfg.config);
+    environment.etc."xdg/kime/config.yaml".text = ''
+      daemon:
+        modules: [${lib.concatStringsSep "," imcfg.kime.daemonModules}]
+      indicator:
+        icon_color: ${imcfg.kime.iconColor}
+    '' + imcfg.kime.extraConfig;
   };
 
   # uses attributes of the linked package
diff --git a/nixos/modules/installer/cd-dvd/channel.nix b/nixos/modules/installer/cd-dvd/channel.nix
index 4b4c2e3933482..8426ba8fac000 100644
--- a/nixos/modules/installer/cd-dvd/channel.nix
+++ b/nixos/modules/installer/cd-dvd/channel.nix
@@ -42,7 +42,7 @@ in
   # see discussion in https://github.com/NixOS/nixpkgs/pull/204178#issuecomment-1336289021
   nix.registry.nixpkgs.to = {
     type = "path";
-    path = nixpkgs;
+    path = "${channelSources}/nixos";
   };
 
   # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
diff --git a/nixos/modules/installer/tools/nixos-install.sh b/nixos/modules/installer/tools/nixos-install.sh
index 9f609cefe6ead..20fec525e70bd 100755
--- a/nixos/modules/installer/tools/nixos-install.sh
+++ b/nixos/modules/installer/tools/nixos-install.sh
@@ -188,17 +188,6 @@ nix-env --store "$mountPoint" "${extraBuildFlags[@]}" \
 mkdir -m 0755 -p "$mountPoint/etc"
 touch "$mountPoint/etc/NIXOS"
 
-# Create a bind mount for each of the mount points inside the target file
-# system. This preserves the validity of their absolute paths after changing
-# the root with `nixos-enter`.
-# Without this the bootloader installation may fail due to options that
-# contain paths referenced during evaluation, like initrd.secrets.
-if (( EUID == 0 )); then
-    mount --rbind --mkdir "$mountPoint" "$mountPoint$mountPoint"
-    mount --make-rslave "$mountPoint$mountPoint"
-    trap 'umount -R "$mountPoint$mountPoint" && rmdir "$mountPoint$mountPoint"' EXIT
-fi
-
 # Switch to the new system configuration.  This will install Grub with
 # a menu default pointing at the kernel/initrd/etc of the new
 # configuration.
@@ -206,7 +195,20 @@ if [[ -z $noBootLoader ]]; then
     echo "installing the boot loader..."
     # Grub needs an mtab.
     ln -sfn /proc/mounts "$mountPoint"/etc/mtab
-    NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root "$mountPoint" -- /run/current-system/bin/switch-to-configuration boot
+    export mountPoint
+    NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root "$mountPoint" -c "$(cat <<'EOF'
+      # Create a bind mount for each of the mount points inside the target file
+      # system. This preserves the validity of their absolute paths after changing
+      # the root with `nixos-enter`.
+      # Without this the bootloader installation may fail due to options that
+      # contain paths referenced during evaluation, like initrd.secrets.
+      # when not root, re-execute the script in an unshared namespace
+      mount --rbind --mkdir / "$mountPoint"
+      mount --make-rslave "$mountPoint"
+      /run/current-system/bin/switch-to-configuration boot
+      umount -R "$mountPoint" && rmdir "$mountPoint"
+EOF
+)"
 fi
 
 # Ask the user to set a root password, but only if the passwd command
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index e44a9899772f1..ecc40ad6adef7 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -357,6 +357,14 @@ in
     (mkIf cfg.nixos.enable {
       system.build.manual = manual;
 
+      system.activationScripts.check-manual-docbook = ''
+        if [[ $(cat ${manual.optionsUsedDocbook}) = 1 ]]; then
+          echo -e "\e[31;1mwarning\e[0m: This configuration contains option documentation in docbook." \
+                  "Support for docbook is deprecated and will be removed after NixOS 23.05." \
+                  "See nix-store --read-log ${builtins.unsafeDiscardStringContext manual.optionsJSON.drvPath}"
+        fi
+      '';
+
       environment.systemPackages = []
         ++ optional cfg.man.enable manual.manpages
         ++ optionals cfg.doc.enable [ manual.manualHTML nixos-help ];
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index dce6e878540d5..e1c174a00f99e 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -530,6 +530,7 @@
   ./services/mail/dovecot.nix
   ./services/mail/dspam.nix
   ./services/mail/exim.nix
+  ./services/mail/goeland.nix
   ./services/mail/listmonk.nix
   ./services/mail/maddy.nix
   ./services/mail/mail.nix
@@ -570,6 +571,7 @@
   ./services/misc/atuin.nix
   ./services/misc/autofs.nix
   ./services/misc/autorandr.nix
+  ./services/misc/autosuspend.nix
   ./services/misc/bazarr.nix
   ./services/misc/beanstalkd.nix
   ./services/misc/bees.nix
diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix
index 06db420632e5f..eb4f11f7dcdec 100644
--- a/nixos/modules/security/acme/default.nix
+++ b/nixos/modules/security/acme/default.nix
@@ -727,7 +727,7 @@ in {
           Default values inheritable by all configured certs. You can
           use this to define options shared by all your certs. These defaults
           can also be ignored on a per-cert basis using the
-          `security.acme.certs.''${cert}.inheritDefaults' option.
+          {option}`security.acme.certs.''${cert}.inheritDefaults` option.
         '';
       };
 
diff --git a/nixos/modules/security/audit.nix b/nixos/modules/security/audit.nix
index 06b4766c8f5a9..afc7dd13039d5 100644
--- a/nixos/modules/security/audit.nix
+++ b/nixos/modules/security/audit.nix
@@ -57,7 +57,7 @@ in {
         type        = types.enum [ false true "lock" ];
         default     = false;
         description = lib.mdDoc ''
-          Whether to enable the Linux audit system. The special `lock' value can be used to
+          Whether to enable the Linux audit system. The special `lock` value can be used to
           enable auditing and prevent disabling it until a restart. Be careful about locking
           this, as it will prevent you from changing your audit configuration until you
           restart. If possible, test your configuration using build-vm beforehand.
diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix
index be04741f4d061..cdf6c22ef1b6a 100644
--- a/nixos/modules/security/systemd-confinement.nix
+++ b/nixos/modules/security/systemd-confinement.nix
@@ -94,7 +94,6 @@ in {
       };
 
       config = let
-        rootName = "${mkPathSafeName name}-chroot";
         inherit (config.confinement) binSh fullUnit;
         wantsAPIVFS = lib.mkDefault (config.confinement.mode == "full-apivfs");
       in lib.mkIf config.confinement.enable {
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
index ba1e4716c9b99..3c853973c8727 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -102,7 +102,7 @@ in {
           Extra directives added to to the end of MPD's configuration file,
           mpd.conf. Basic configuration like file location and uid/gid
           is added automatically to the beginning of the file. For available
-          options see `man 5 mpd.conf`'.
+          options see {manpage}`mpd.conf(5)`.
         '';
       };
 
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index 0d21b1e8d66a8..bc24e13aa050e 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -126,6 +126,21 @@ in
           ];
         };
 
+        exclude = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          description = lib.mdDoc ''
+            Patterns to exclude when backing up. See
+            https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
+            details on syntax.
+          '';
+          example = [
+            "/var/cache"
+            "/home/*/.cache"
+            ".git"
+          ];
+        };
+
         timerConfig = mkOption {
           type = types.attrsOf unitOption;
           default = {
@@ -249,6 +264,7 @@ in
     example = {
       localbackup = {
         paths = [ "/home" ];
+        exclude = [ "/home/*/.cache" ];
         repository = "/mnt/backup-hdd";
         passwordFile = "/etc/nixos/secrets/restic-password";
         initialize = true;
@@ -270,12 +286,17 @@ in
 
   config = {
     warnings = mapAttrsToList (n: v: "services.restic.backups.${n}.s3CredentialsFile is deprecated, please use services.restic.backups.${n}.environmentFile instead.") (filterAttrs (n: v: v.s3CredentialsFile != null) config.services.restic.backups);
+    assertions = mapAttrsToList (n: v: {
+      assertion = (v.repository == null) != (v.repositoryFile == null);
+      message = "services.restic.backups.${n}: exactly one of repository or repositoryFile should be set";
+    }) config.services.restic.backups;
     systemd.services =
       mapAttrs'
         (name: backup:
           let
             extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
             resticCmd = "${backup.package}/bin/restic${extraOptions}";
+            excludeFlags = if (backup.exclude != []) then ["--exclude-file=${pkgs.writeText "exclude-patterns" (concatStringsSep "\n" backup.exclude)}"] else [];
             filesFromTmpFile = "/run/restic-backups-${name}/includes";
             backupPaths =
               if (backup.dynamicFilesFrom == null)
@@ -311,7 +332,7 @@ in
             restartIfChanged = false;
             serviceConfig = {
               Type = "oneshot";
-              ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ])
+              ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " (backup.extraBackupArgs ++ excludeFlags)} ${backupPaths}" ])
                 ++ pruneCmd;
               User = backup.user;
               RuntimeDirectory = "restic-backups-${name}";
diff --git a/nixos/modules/services/mail/goeland.nix b/nixos/modules/services/mail/goeland.nix
new file mode 100644
index 0000000000000..13092a65ed90e
--- /dev/null
+++ b/nixos/modules/services/mail/goeland.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.goeland;
+  tomlFormat = pkgs.formats.toml { };
+in
+{
+  options.services.goeland = {
+    enable = mkEnableOption (mdDoc "goeland");
+
+    settings = mkOption {
+      description = mdDoc ''
+        Configuration of goeland.
+        See the [example config file](https://github.com/slurdge/goeland/blob/master/cmd/asset/config.default.toml) for the available options.
+      '';
+      default = { };
+      type = tomlFormat.type;
+    };
+    schedule = mkOption {
+      type = types.str;
+      default = "12h";
+      example = "Mon, 00:00:00";
+      description = mdDoc "How often to run goeland, in systemd time format.";
+    };
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/lib/goeland";
+      description = mdDoc ''
+        The data directory for goeland where the database will reside if using the unseen filter.
+        If left as the default value this directory will automatically be created before the goeland
+        server starts, otherwise you are responsible for ensuring the directory exists with
+        appropriate ownership and permissions.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.goeland.settings.database = "${cfg.stateDir}/goeland.db";
+
+    systemd.services.goeland = {
+      serviceConfig = let confFile = tomlFormat.generate "config.toml" cfg.settings; in mkMerge [
+        {
+          ExecStart = "${pkgs.goeland}/bin/goeland run -c ${confFile}";
+          User = "goeland";
+          Group = "goeland";
+        }
+        (mkIf (cfg.stateDir == "/var/lib/goeland") {
+          StateDirectory = "goeland";
+          StateDirectoryMode = "0750";
+        })
+      ];
+      startAt = cfg.schedule;
+    };
+
+    users.users.goeland = {
+      description = "goeland user";
+      group = "goeland";
+      isSystemUser = true;
+    };
+    users.groups.goeland = { };
+
+    warnings = optionals (hasAttr "password" cfg.settings.email) [
+      ''
+        It is not recommended to set the "services.goeland.settings.email.password"
+        option as it will be in cleartext in the Nix store.
+        Please use "services.goeland.settings.email.password_file" instead.
+      ''
+    ];
+  };
+
+  meta.maintainers = with maintainers; [ sweenu ];
+}
diff --git a/nixos/modules/services/misc/autosuspend.nix b/nixos/modules/services/misc/autosuspend.nix
new file mode 100644
index 0000000000000..b3e362533a093
--- /dev/null
+++ b/nixos/modules/services/misc/autosuspend.nix
@@ -0,0 +1,230 @@
+{ config, pkgs, lib, ... }:
+let
+  inherit (lib) mapAttrs' nameValuePair filterAttrs types mkEnableOption
+    mdDoc mkPackageOptionMD mkOption literalExpression mkIf flatten
+    maintainers attrValues;
+
+  cfg = config.services.autosuspend;
+
+  settingsFormat = pkgs.formats.ini { };
+
+  checks =
+    mapAttrs'
+      (n: v: nameValuePair "check.${n}" (filterAttrs (_: v: v != null) v))
+      cfg.checks;
+  wakeups =
+    mapAttrs'
+      (n: v: nameValuePair "wakeup.${n}" (filterAttrs (_: v: v != null) v))
+      cfg.wakeups;
+
+  # Whether the given check is enabled
+  hasCheck = class:
+    (filterAttrs
+      (n: v: v.enabled && (if v.class == null then n else v.class) == class)
+      cfg.checks)
+    != { };
+
+  # Dependencies needed by specific checks
+  dependenciesForChecks = {
+    "Smb" = pkgs.samba;
+    "XIdleTime" = [ pkgs.xprintidle pkgs.sudo ];
+  };
+
+  autosuspend-conf =
+    settingsFormat.generate "autosuspend.conf" ({ general = cfg.settings; } // checks // wakeups);
+
+  autosuspend = cfg.package;
+
+  checkType = types.submodule {
+    freeformType = settingsFormat.type.nestedTypes.elemType;
+
+    options.enabled = mkEnableOption (mdDoc "this activity check") // { default = true; };
+
+    options.class = mkOption {
+      default = null;
+      type = with types; nullOr (enum [
+        "ActiveCalendarEvent"
+        "ActiveConnection"
+        "ExternalCommand"
+        "JsonPath"
+        "Kodi"
+        "KodiIdleTime"
+        "LastLogActivity"
+        "Load"
+        "LogindSessionsIdle"
+        "Mpd"
+        "NetworkBandwidth"
+        "Ping"
+        "Processes"
+        "Smb"
+        "Users"
+        "XIdleTime"
+        "XPath"
+      ]);
+      description = mdDoc ''
+        Name of the class implementing the check.  If this option is not specified, the check's
+        name must represent a valid internal check class.
+      '';
+    };
+  };
+
+  wakeupType = types.submodule {
+    freeformType = settingsFormat.type.nestedTypes.elemType;
+
+    options.enabled = mkEnableOption (mdDoc "this wake-up check") // { default = true; };
+
+    options.class = mkOption {
+      default = null;
+      type = with types; nullOr (enum [
+        "Calendar"
+        "Command"
+        "File"
+        "Periodic"
+        "SystemdTimer"
+        "XPath"
+        "XPathDelta"
+      ]);
+      description = mdDoc ''
+        Name of the class implementing the check.  If this option is not specified, the check's
+        name must represent a valid internal check class.
+      '';
+    };
+  };
+in
+{
+  options = {
+    services.autosuspend = {
+      enable = mkEnableOption (mdDoc "the autosuspend daemon");
+
+      package = mkPackageOptionMD pkgs "autosuspend" { };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = settingsFormat.type.nestedTypes.elemType;
+
+          options = {
+            # Provide reasonable defaults for these two (required) options
+            suspend_cmd = mkOption {
+              default = "systemctl suspend";
+              type = with types; str;
+              description = mdDoc ''
+                The command to execute in case the host shall be suspended. This line can contain
+                additional command line arguments to the command to execute.
+              '';
+            };
+            wakeup_cmd = mkOption {
+              default = ''sh -c 'echo 0 > /sys/class/rtc/rtc0/wakealarm && echo {timestamp:.0f} > /sys/class/rtc/rtc0/wakealarm' '';
+              type = with types; str;
+              description = mdDoc ''
+                The command to execute for scheduling a wake up of the system. The given string is
+                processed using Python’s `str.format()` and a format argument called `timestamp`
+                encodes the UTC timestamp of the planned wake up time (float). Additionally `iso`
+                can be used to acquire the timestamp in ISO 8601 format.
+              '';
+            };
+          };
+        };
+        default = { };
+        example = literalExpression ''
+          {
+            enable = true;
+            interval = 30;
+            idle_time = 120;
+          }
+        '';
+        description = mdDoc ''
+          Configuration for autosuspend, see
+          <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#general-configuration>
+          for supported values.
+        '';
+      };
+
+      checks = mkOption {
+        default = { };
+        type = with types; attrsOf checkType;
+        description = mdDoc ''
+          Checks for activity.  For more information, see:
+           - <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#activity-check-configuration>
+           - <https://autosuspend.readthedocs.io/en/latest/available_checks.html>
+        '';
+        example = literalExpression ''
+          {
+            # Basic activity check configuration.
+            # The check class name is derived from the section header (Ping in this case).
+            # Remember to enable desired checks. They are disabled by default.
+            Ping = {
+              hosts = "192.168.0.7";
+            };
+
+            # This check is disabled.
+            Smb.enabled = false;
+
+            # Example for a custom check name.
+            # This will use the Users check with the custom name RemoteUsers.
+            # Custom names are necessary in case a check class is used multiple times.
+            # Custom names can also be used for clarification.
+            RemoteUsers = {
+              class = "Users";
+              name = ".*";
+              terminal = ".*";
+              host = "[0-9].*";
+            };
+
+            # Here the Users activity check is used again with different settings and a different name
+            LocalUsers = {
+              class = "Users";
+              name = ".*";
+              terminal = ".*";
+              host = "localhost";
+            };
+          }
+        '';
+      };
+
+      wakeups = mkOption {
+        default = { };
+        type = with types; attrsOf wakeupType;
+        description = mdDoc ''
+          Checks for wake up.  For more information, see:
+           - <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#wake-up-check-configuration>
+           - <https://autosuspend.readthedocs.io/en/latest/available_wakeups.html>
+        '';
+        example = literalExpression ''
+          {
+            # Wake up checks reuse the same configuration mechanism as activity checks.
+            Calendar = {
+              url = "http://example.org/test.ics";
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.autosuspend = {
+      description = "A daemon to suspend your server in case of inactivity";
+      documentation = [ "https://autosuspend.readthedocs.io/en/latest/systemd_integration.html" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = flatten (attrValues (filterAttrs (n: _: hasCheck n) dependenciesForChecks));
+      serviceConfig = {
+        ExecStart = ''${autosuspend}/bin/autosuspend -l ${autosuspend}/etc/autosuspend-logging.conf -c ${autosuspend-conf} daemon'';
+      };
+    };
+
+    systemd.services.autosuspend-detect-suspend = {
+      description = "Notifies autosuspend about suspension";
+      documentation = [ "https://autosuspend.readthedocs.io/en/latest/systemd_integration.html" ];
+      wantedBy = [ "sleep.target" ];
+      after = [ "sleep.target" ];
+      serviceConfig = {
+        ExecStart = ''${autosuspend}/bin/autosuspend -l ${autosuspend}/etc/autosuspend-logging.conf -c ${autosuspend-conf} presuspend'';
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ xlambein ];
+  };
+}
diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix
index ec0a8e1eaa1c4..9b03b6f9662ec 100644
--- a/nixos/modules/services/misc/gpsd.nix
+++ b/nixos/modules/services/misc/gpsd.nix
@@ -22,7 +22,7 @@ in
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to enable `gpsd', a GPS service daemon.
+          Whether to enable `gpsd`, a GPS service daemon.
         '';
       };
 
diff --git a/nixos/modules/services/misc/weechat.nix b/nixos/modules/services/misc/weechat.nix
index 663a767a0c18b..aa5b9b22837e2 100644
--- a/nixos/modules/services/misc/weechat.nix
+++ b/nixos/modules/services/misc/weechat.nix
@@ -15,7 +15,7 @@ in
       default = "/var/lib/weechat";
     };
     sessionName = mkOption {
-      description = lib.mdDoc "Name of the `screen' session for weechat.";
+      description = lib.mdDoc "Name of the `screen` session for weechat.";
       default = "weechat-screen";
       type = types.str;
     };
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
index 60f6e84c63c7d..7ea17b07567cd 100644
--- a/nixos/modules/services/monitoring/grafana-image-renderer.nix
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -108,7 +108,7 @@ in {
 
     services.grafana.settings.rendering = mkIf cfg.provisionGrafana {
       url = "http://localhost:${toString cfg.settings.service.port}/render";
-      callback_url = "http://localhost:${toString config.services.grafana.port}";
+      callback_url = "http://localhost:${toString config.services.grafana.settings.server.http_port}";
     };
 
     services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
diff --git a/nixos/modules/services/monitoring/tuptime.nix b/nixos/modules/services/monitoring/tuptime.nix
index d97e408bce319..97cc375262546 100644
--- a/nixos/modules/services/monitoring/tuptime.nix
+++ b/nixos/modules/services/monitoring/tuptime.nix
@@ -54,8 +54,8 @@ in {
             Type = "oneshot";
             User = "_tuptime";
             RemainAfterExit = true;
-            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
-            ExecStop = "${pkgs.tuptime}/bin/tuptime -xg";
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -q";
+            ExecStop = "${pkgs.tuptime}/bin/tuptime -qg";
           };
         };
 
@@ -64,7 +64,7 @@ in {
           serviceConfig = {
             Type = "oneshot";
             User = "_tuptime";
-            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -q";
           };
         };
       };
diff --git a/nixos/modules/services/monitoring/uptime-kuma.nix b/nixos/modules/services/monitoring/uptime-kuma.nix
index 3dfbfe3652cfa..886e14b5f6c9f 100644
--- a/nixos/modules/services/monitoring/uptime-kuma.nix
+++ b/nixos/modules/services/monitoring/uptime-kuma.nix
@@ -7,6 +7,8 @@ let
 in
 {
 
+  meta.maintainers = [ lib.maintainers.julienmalka ];
+
   options = {
     services.uptime-kuma = {
       enable = mkEnableOption (mdDoc "Uptime Kuma, this assumes a reverse proxy to be set.");
@@ -48,7 +50,7 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       environment = cfg.settings;
-      path = lib.mkIf cfg.appriseSupport (with pkgs; [ apprise ]);
+      path = with pkgs; [ unixtools.ping ] ++ lib.optional cfg.appriseSupport apprise;
       serviceConfig = {
         Type = "simple";
         StateDirectory = "uptime-kuma";
diff --git a/nixos/modules/services/networking/avahi-daemon.nix b/nixos/modules/services/networking/avahi-daemon.nix
index 3933ed5a2315a..28dd24f4e7c0c 100644
--- a/nixos/modules/services/networking/avahi-daemon.nix
+++ b/nixos/modules/services/networking/avahi-daemon.nix
@@ -47,7 +47,7 @@ in
         Whether to run the Avahi daemon, which allows Avahi clients
         to use Avahi's service discovery facilities and also allows
         the local machine to advertise its presence and services
-        (through the mDNS responder implemented by `avahi-daemon').
+        (through the mDNS responder implemented by `avahi-daemon`).
       '';
     };
 
@@ -205,7 +205,7 @@ in
       default = false;
       description = lib.mdDoc ''
         Whether to enable the mDNS NSS (Name Service Switch) plug-in.
-        Enabling it allows applications to resolve names in the `.local'
+        Enabling it allows applications to resolve names in the `.local`
         domain by transparently querying the Avahi daemon.
       '';
     };
diff --git a/nixos/modules/services/networking/gnunet.nix b/nixos/modules/services/networking/gnunet.nix
index 9d1c9746f7285..301fe021b0aa6 100644
--- a/nixos/modules/services/networking/gnunet.nix
+++ b/nixos/modules/services/networking/gnunet.nix
@@ -124,8 +124,8 @@ in
         type = types.lines;
         default = "";
         description = lib.mdDoc ''
-          Additional options that will be copied verbatim in `gnunet.conf'.
-          See `gnunet.conf(5)' for details.
+          Additional options that will be copied verbatim in `gnunet.conf`.
+          See {manpage}`gnunet.conf(5)` for details.
         '';
       };
     };
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
index a6f403b46f875..3afe6fe0a9711 100644
--- a/nixos/modules/services/networking/nat.nix
+++ b/nixos/modules/services/networking/nat.nix
@@ -124,7 +124,7 @@ in
             type = types.listOf types.str;
             default = [ ];
             example = literalExpression ''[ "55.1.2.3" ]'';
-            description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT";
+            description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort` from the host itself and from other hosts behind NAT";
           };
         };
       });
diff --git a/nixos/modules/services/networking/nomad.nix b/nixos/modules/services/networking/nomad.nix
index c6f0624c8ceb0..b1e51195247a5 100644
--- a/nixos/modules/services/networking/nomad.nix
+++ b/nixos/modules/services/networking/nomad.nix
@@ -71,6 +71,17 @@ in
         '';
       };
 
+      credentials = mkOption {
+        description = lib.mdDoc ''
+          Credentials envs used to configure nomad secrets.
+        '';
+        type = types.attrsOf types.str;
+        default = { };
+
+        example = {
+          logs_remote_write_password = "/run/keys/nomad_write_password";
+        };
+      };
 
       settings = mkOption {
         type = format.type;
@@ -148,7 +159,8 @@ in
                 };
             in
             "${cfg.package}/bin/nomad agent -config=/etc/nomad.json -plugin-dir=${pluginsDir}/bin" +
-            concatMapStrings (path: " -config=${path}") cfg.extraSettingsPaths;
+            concatMapStrings (path: " -config=${path}") cfg.extraSettingsPaths +
+            concatMapStrings (key: " -config=\${CREDENTIALS_DIRECTORY}/${key}") (lib.attrNames cfg.credentials);
           KillMode = "process";
           KillSignal = "SIGINT";
           LimitNOFILE = 65536;
@@ -157,6 +169,7 @@ in
           Restart = "on-failure";
           RestartSec = 2;
           TasksMax = "infinity";
+          LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials;
         }
         (mkIf cfg.enableDocker {
           SupplementaryGroups = "docker"; # space-separated string
diff --git a/nixos/modules/services/networking/redsocks.nix b/nixos/modules/services/networking/redsocks.nix
index 45feb1313c924..30d6a0a6336d4 100644
--- a/nixos/modules/services/networking/redsocks.nix
+++ b/nixos/modules/services/networking/redsocks.nix
@@ -37,7 +37,7 @@ in
               - stderr
               - file:/path/to/file
               - syslog:FACILITY where FACILITY is any of "daemon", "local0",
-              etc.
+                etc.
           '';
       };
 
@@ -125,6 +125,7 @@ in
               lib.mdDoc ''
                 Way to disclose client IP to the proxy.
                   - "false": do not disclose
+
                 http-connect supports the following ways:
                   - "X-Forwarded-For": add header "X-Forwarded-For: IP"
                   - "Forwarded_ip": add header "Forwarded: for=IP" (see RFC7239)
diff --git a/nixos/modules/services/networking/rpcbind.nix b/nixos/modules/services/networking/rpcbind.nix
index 60e78dfec51ba..63c4859fbd07a 100644
--- a/nixos/modules/services/networking/rpcbind.nix
+++ b/nixos/modules/services/networking/rpcbind.nix
@@ -14,7 +14,7 @@ with lib;
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to enable `rpcbind', an ONC RPC directory service
+          Whether to enable `rpcbind`, an ONC RPC directory service
           notably used by NFS and NIS, and which can be queried
           using the rpcinfo(1) command. `rpcbind` is a replacement for
           `portmap`.
diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix
index 41c4ec2d2951e..7932bac9ca3a1 100644
--- a/nixos/modules/services/networking/ssh/lshd.nix
+++ b/nixos/modules/services/networking/ssh/lshd.nix
@@ -40,7 +40,7 @@ in
         type = types.listOf types.str;
         description = lib.mdDoc ''
           List of network interfaces where listening for connections.
-          When providing the empty list, `[]', lshd listens on all
+          When providing the empty list, `[]`, lshd listens on all
           network interfaces.
         '';
         example = [ "localhost" "1.2.3.4:443" ];
diff --git a/nixos/modules/services/networking/tmate-ssh-server.nix b/nixos/modules/services/networking/tmate-ssh-server.nix
index f7740b1ddfccb..ff4ce0773309f 100644
--- a/nixos/modules/services/networking/tmate-ssh-server.nix
+++ b/nixos/modules/services/networking/tmate-ssh-server.nix
@@ -28,7 +28,7 @@ in
     host = mkOption {
       type = types.str;
       description = mdDoc "External host name";
-      defaultText = lib.literalExpression "config.networking.domain or config.networking.hostName ";
+      defaultText = lib.literalExpression "config.networking.domain or config.networking.hostName";
       default =
         if domain == null then
           config.networking.hostName
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
index 6207f9dae971d..3c4bcd1ac2659 100644
--- a/nixos/modules/services/security/fail2ban.nix
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -86,7 +86,7 @@ in
 
       banaction = mkOption {
         default = if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport";
-        defaultText = literalExpression '' if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport" '';
+        defaultText = literalExpression ''if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport"'';
         type = types.str;
         description = lib.mdDoc ''
           Default banning action (e.g. iptables, iptables-new, iptables-multiport,
@@ -98,7 +98,7 @@ in
 
       banaction-allports = mkOption {
         default = if config.networking.nftables.enable then "nftables-allport" else "iptables-allport";
-        defaultText = literalExpression '' if config.networking.nftables.enable then "nftables-allport" else "iptables-allport" '';
+        defaultText = literalExpression ''if config.networking.nftables.enable then "nftables-allport" else "iptables-allport"'';
         type = types.str;
         description = lib.mdDoc ''
           Default banning action (e.g. iptables, iptables-new, iptables-multiport,
diff --git a/nixos/modules/services/torrent/magnetico.nix b/nixos/modules/services/torrent/magnetico.nix
index b813f1205119c..dc6b4e9aa734d 100644
--- a/nixos/modules/services/torrent/magnetico.nix
+++ b/nixos/modules/services/torrent/magnetico.nix
@@ -144,7 +144,7 @@ in {
         interface. If unset no authentication will be required.
 
         The file must contain user names and password hashes in the format
-        `username:hash `, one for each line.  Usernames must
+        `username:hash`, one for each line.  Usernames must
         start with a lowecase ([a-z]) ASCII character, might contain
         non-consecutive underscores except at the end, and consists of
         small-case a-z characters and digits 0-9.
diff --git a/nixos/modules/services/web-apps/hedgedoc.nix b/nixos/modules/services/web-apps/hedgedoc.nix
index 90ca3002c5924..a7823354ce889 100644
--- a/nixos/modules/services/web-apps/hedgedoc.nix
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -950,16 +950,16 @@ in
                 type = types.str;
                 default = "";
                 description = lib.mdDoc ''
-                  Attribute map for `id'.
-                  Defaults to `NameID' of SAML response.
+                  Attribute map for `id`.
+                  Defaults to `NameID` of SAML response.
                 '';
               };
               username = mkOption {
                 type = types.str;
                 default = "";
                 description = lib.mdDoc ''
-                  Attribute map for `username'.
-                  Defaults to `NameID' of SAML response.
+                  Attribute map for `username`.
+                  Defaults to `NameID` of SAML response.
                 '';
               };
               email = mkOption {
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 6fafae8928a67..c723b962c8474 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -288,7 +288,7 @@ let
 
   configPath = if cfg.enableReload
     then "/etc/nginx/nginx.conf"
-    else finalConfigFile;
+    else configFile;
 
   execCommand = "${cfg.package}/bin/nginx -c '${configPath}'";
 
@@ -440,38 +440,6 @@ let
   );
 
   mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
-
-  snakeOilCert = pkgs.runCommand "nginx-config-validate-cert" { nativeBuildInputs = [ pkgs.openssl.bin ]; } ''
-    mkdir $out
-    openssl genrsa -des3 -passout pass:xxxxx -out server.pass.key 2048
-    openssl rsa -passin pass:xxxxx -in server.pass.key -out $out/server.key
-    openssl req -new -key $out/server.key -out server.csr \
-    -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
-    openssl x509 -req -days 1 -in server.csr -signkey $out/server.key -out $out/server.crt
-  '';
-  validatedConfigFile = pkgs.runCommand "validated-nginx.conf" { nativeBuildInputs = [ cfg.package ]; } ''
-    # nginx absolutely wants to read the certificates even when told to only validate config, so let's provide fake certs
-    sed ${configFile} \
-    -e "s|ssl_certificate .*;|ssl_certificate ${snakeOilCert}/server.crt;|g" \
-    -e "s|ssl_trusted_certificate .*;|ssl_trusted_certificate ${snakeOilCert}/server.crt;|g" \
-    -e "s|ssl_certificate_key .*;|ssl_certificate_key ${snakeOilCert}/server.key;|g" \
-    > conf
-
-    LD_PRELOAD=${pkgs.libredirect}/lib/libredirect.so \
-      NIX_REDIRECTS="/etc/resolv.conf=/dev/null" \
-      nginx -t -c $(readlink -f ./conf) > out 2>&1 || true
-    if ! grep -q "syntax is ok" out; then
-      echo nginx config validation failed.
-      echo config was ${configFile}.
-      echo 'in case of false positive, set `services.nginx.validateConfig` to false.'
-      echo nginx output:
-      cat out
-      exit 1
-    fi
-    cp ${configFile} $out
-  '';
-
-  finalConfigFile = if cfg.validateConfig then validatedConfigFile else configFile;
 in
 
 {
@@ -580,17 +548,6 @@ in
         '';
       };
 
-      validateConfig = mkOption {
-        # FIXME: re-enable if we can make of the configurations work.
-        #default = pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform;
-        default = false;
-        defaultText = literalExpression "pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform";
-        type = types.bool;
-        description = lib.mdDoc ''
-          Validate the generated nginx config at build time. The check is not very robust and can be disabled in case of false positives. This is notably the case when cross-compiling or when using `include` with files outside of the store.
-        '';
-      };
-
       additionalModules = mkOption {
         default = [];
         type = types.listOf (types.attrsOf types.anything);
@@ -1128,7 +1085,7 @@ in
     };
 
     environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload {
-      source = finalConfigFile;
+      source = configFile;
     };
 
     # This service waits for all certificates to be available
@@ -1147,7 +1104,7 @@ in
       # certs are updated _after_ config has been reloaded.
       before = sslTargets;
       after = sslServices;
-      restartTriggers = optionals cfg.enableReload [ finalConfigFile ];
+      restartTriggers = optionals cfg.enableReload [ configFile ];
       # Block reloading if not all certs exist yet.
       # Happens when config changes add new vhosts/certs.
       unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames);
diff --git a/nixos/modules/system/boot/loader/init-script/init-script.nix b/nixos/modules/system/boot/loader/init-script/init-script.nix
index 908f8b8e8c499..4d33ed6b665b9 100644
--- a/nixos/modules/system/boot/loader/init-script/init-script.nix
+++ b/nixos/modules/system/boot/loader/init-script/init-script.nix
@@ -8,7 +8,7 @@ let
     src = ./init-script-builder.sh;
     isExecutable = true;
     inherit (pkgs) bash;
-    inherit (config.nixos.system) distroName;
+    inherit (config.system.nixos) distroName;
     path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
   };
 
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 ea3577f138c29..3e3683211f1e0 100755
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
@@ -42,7 +42,7 @@ def system_dir(profile: Optional[str], generation: int, specialisation: Optional
     else:
         return d
 
-BOOT_ENTRY = """title @distroName@{profile}{specialisation}
+BOOT_ENTRY = """title {title}
 version Generation {generation} {description}
 linux {kernel}
 initrd {initrd}
@@ -106,14 +106,29 @@ def describe_generation(generation_dir: str) -> str:
     return description
 
 
-def write_entry(profile: Optional[str], generation: int, specialisation: Optional[str], machine_id: str) -> None:
+def write_entry(profile: Optional[str], generation: int, specialisation: Optional[str],
+                machine_id: str, current: bool) -> None:
     kernel = copy_from_profile(profile, generation, specialisation, "kernel")
     initrd = copy_from_profile(profile, generation, specialisation, "initrd")
+
+    title = "@distroName@{profile}{specialisation}".format(
+        profile=" [" + profile + "]" if profile else "",
+        specialisation=" (%s)" % specialisation if specialisation else "")
+
     try:
         append_initrd_secrets = profile_path(profile, generation, specialisation, "append-initrd-secrets")
         subprocess.check_call([append_initrd_secrets, "@efiSysMountPoint@%s" % (initrd)])
     except FileNotFoundError:
         pass
+    except subprocess.CalledProcessError:
+        if current:
+            print("failed to create initrd secrets!", file=sys.stderr)
+            sys.exit(1)
+        else:
+            print("warning: failed to create initrd secrets "
+                  f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr)
+            print("note: this is normal after having removed "
+                  "or renamed a file in `boot.initrd.secrets`", file=sys.stderr)
     entry_file = "@efiSysMountPoint@/loader/entries/%s" % (
         generation_conf_filename(profile, generation, specialisation))
     generation_dir = os.readlink(system_dir(profile, generation, specialisation))
@@ -123,8 +138,7 @@ def write_entry(profile: Optional[str], generation: int, specialisation: Optiona
     with open("%s/kernel-params" % (generation_dir)) as params_file:
         kernel_params = kernel_params + params_file.read()
     with open(tmp_path, 'w') as f:
-        f.write(BOOT_ENTRY.format(profile=" [" + profile + "]" if profile else "",
-                    specialisation=" (%s)" % specialisation if specialisation else "",
+        f.write(BOOT_ENTRY.format(title=title,
                     generation=generation,
                     kernel=kernel,
                     initrd=initrd,
@@ -228,20 +242,21 @@ def main() -> None:
         warnings.warn("NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER", DeprecationWarning)
         os.environ["NIXOS_INSTALL_BOOTLOADER"] = "1"
 
+    # flags to pass to bootctl install/update
+    bootctl_flags = []
+
+    if "@canTouchEfiVariables@" != "1":
+        bootctl_flags.append("--no-variables")
+
+    if "@graceful@" == "1":
+        bootctl_flags.append("--graceful")
+
     if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1":
         # bootctl uses fopen() with modes "wxe" and fails if the file exists.
         if os.path.exists("@efiSysMountPoint@/loader/loader.conf"):
             os.unlink("@efiSysMountPoint@/loader/loader.conf")
 
-        flags = []
-
-        if "@canTouchEfiVariables@" != "1":
-            flags.append("--no-variables")
-
-        if "@graceful@" == "1":
-            flags.append("--graceful")
-
-        subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + flags + ["install"])
+        subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + bootctl_flags + ["install"])
     else:
         # Update bootloader to latest if needed
         available_out = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[2]
@@ -270,7 +285,7 @@ def main() -> None:
                 print("skipping systemd-boot update to %s because of known regression" % available_version)
             else:
                 print("updating systemd-boot from %s to %s" % (installed_version, available_version))
-                subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@", "update"])
+                subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + bootctl_flags + ["update"])
 
     mkdir_p("@efiSysMountPoint@/efi/nixos")
     mkdir_p("@efiSysMountPoint@/loader/entries")
@@ -281,10 +296,11 @@ def main() -> None:
     remove_old_entries(gens)
     for gen in gens:
         try:
-            write_entry(*gen, machine_id)
+            is_default = os.readlink(system_dir(*gen)) == args.default_config
+            write_entry(*gen, machine_id, current=is_default)
             for specialisation in get_specialisations(*gen):
-                write_entry(*specialisation, machine_id)
-            if os.readlink(system_dir(*gen)) == args.default_config:
+                write_entry(*specialisation, machine_id, current=is_default)
+            if is_default:
                 write_loader_conf(*gen)
         except OSError as e:
             profile = f"profile '{gen.profile}'" if gen.profile else "default profile"
diff --git a/nixos/modules/tasks/filesystems/envfs.nix b/nixos/modules/tasks/filesystems/envfs.nix
index ef8f655c532a9..450b805f0f580 100644
--- a/nixos/modules/tasks/filesystems/envfs.nix
+++ b/nixos/modules/tasks/filesystems/envfs.nix
@@ -35,7 +35,7 @@ in {
         type = lib.types.package;
         description = lib.mdDoc "Which package to use for the envfs.";
         default = pkgs.envfs;
-        defaultText = lib.mdDoc "pkgs.envfs";
+        defaultText = lib.literalExpression "pkgs.envfs";
       };
     };
   };
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index 4520408ca3379..06210529eb8c4 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -211,7 +211,7 @@ let
             ''
               mkdir $out
               diskImage=$out/disk.img
-              ${qemu}/bin/qemu-img create -f qcow2 $diskImage "60M"
+              ${qemu}/bin/qemu-img create -f qcow2 $diskImage "120M"
               ${if cfg.useEFIBoot then ''
                 efiVars=$out/efi-vars.fd
                 cp ${cfg.efi.variables} $efiVars
@@ -225,7 +225,7 @@ let
                       + " -drive if=pflash,format=raw,unit=1,file=$efiVars");
         }
         ''
-          # Create a /boot EFI partition with 60M and arbitrary but fixed GUIDs for reproducibility
+          # Create a /boot EFI partition with 120M and arbitrary but fixed GUIDs for reproducibility
           ${pkgs.gptfdisk}/bin/sgdisk \
             --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \
             --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \
diff --git a/nixos/tests/ceph-single-node.nix b/nixos/tests/ceph-single-node.nix
index 4fe5dc59ff8f3..4a5636fac1563 100644
--- a/nixos/tests/ceph-single-node.nix
+++ b/nixos/tests/ceph-single-node.nix
@@ -181,6 +181,17 @@ let
     monA.wait_until_succeeds("ceph osd stat | grep -e '3 osds: 3 up[^,]*, 3 in'")
     monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
+
+    # Enable the dashboard and recheck health
+    monA.succeed(
+        "ceph mgr module enable dashboard",
+        "ceph config set mgr mgr/dashboard/ssl false",
+        # default is 8080 but it's better to be explicit
+        "ceph config set mgr mgr/dashboard/server_port 8080",
+    )
+    monA.wait_for_open_port(8080)
+    monA.wait_until_succeeds("curl -q --fail http://localhost:8080")
+    monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
   '';
 in {
   name = "basic-single-node-ceph-cluster";
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
index 50b85560e12cb..3adfa979edcc7 100644
--- a/nixos/tests/installer.nix
+++ b/nixos/tests/installer.nix
@@ -51,7 +51,7 @@ let
           boot.loader.systemd-boot.enable = true;
         ''}
 
-        boot.initrd.secrets."/etc/secret" = /etc/nixos/secret;
+        boot.initrd.secrets."/etc/secret" = ./secret;
 
         users.users.alice = {
           isNormalUser = true;
diff --git a/nixos/tests/nginx.nix b/nixos/tests/nginx.nix
index 73f1133bd6ca9..d9d073822a145 100644
--- a/nixos/tests/nginx.nix
+++ b/nixos/tests/nginx.nix
@@ -61,7 +61,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
       specialisation.reloadWithErrorsSystem.configuration = {
         services.nginx.package = pkgs.nginxMainline;
-        services.nginx.virtualHosts."hello".extraConfig = "access_log /does/not/exist.log;";
+        services.nginx.virtualHosts."!@$$(#*%".locations."~@#*$*!)".proxyPass = ";;;";
       };
     };
   };
diff --git a/nixos/tests/pantheon.nix b/nixos/tests/pantheon.nix
index 52f85f5c07da8..0773fc0472aa3 100644
--- a/nixos/tests/pantheon.nix
+++ b/nixos/tests/pantheon.nix
@@ -20,8 +20,8 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
   enableOCR = true;
 
   testScript = { nodes, ... }: let
-    user = nodes.machine.config.users.users.alice;
-    bob = nodes.machine.config.users.users.bob;
+    user = nodes.machine.users.users.alice;
+    bob = nodes.machine.users.users.bob;
   in ''
     machine.wait_for_unit("display-manager.service")
 
@@ -40,7 +40,6 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
     with subtest("Check that logging in has given the user ownership of devices"):
         machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
 
-    # TODO: DBus API could eliminate this? Pantheon uses Bamf.
     with subtest("Check if pantheon session components actually start"):
         machine.wait_until_succeeds("pgrep gala")
         machine.wait_for_window("gala")
@@ -49,6 +48,12 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
         machine.wait_until_succeeds("pgrep plank")
         machine.wait_for_window("plank")
 
+    with subtest("Open system settings"):
+        machine.execute("su - ${user.name} -c 'DISPLAY=:0 io.elementary.switchboard >&2 &'")
+        # Wait for all plugins to be loaded before we check if the window is still there.
+        machine.sleep(5)
+        machine.wait_for_window("io.elementary.switchboard")
+
     with subtest("Open elementary terminal"):
         machine.execute("su - ${user.name} -c 'DISPLAY=:0 io.elementary.terminal >&2 &'")
         machine.wait_for_window("io.elementary.terminal")
diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix
index 3681c4cf190e3..42af0783863ea 100644
--- a/nixos/tests/restic.nix
+++ b/nixos/tests/restic.nix
@@ -7,17 +7,27 @@ import ./make-test-python.nix (
     rcloneRepository = "rclone:local:/tmp/restic-rclone-backup";
 
     backupPrepareCommand = ''
-      touch /opt/backupPrepareCommand
-      test ! -e /opt/backupCleanupCommand
+      touch /tmp/backupPrepareCommand
+      test ! -e /tmp/backupCleanupCommand
     '';
 
     backupCleanupCommand = ''
-      rm /opt/backupPrepareCommand
-      touch /opt/backupCleanupCommand
+      rm /tmp/backupPrepareCommand
+      touch /tmp/backupCleanupCommand
     '';
 
+    testDir = pkgs.stdenvNoCC.mkDerivation {
+      name = "test-files-to-backup";
+      unpackPhase = "true";
+      installPhase = ''
+        mkdir $out
+        touch $out/some_file
+      '';
+    };
+
     passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}";
     paths = [ "/opt" ];
+    exclude = [ "/opt/excluded_file_*" ];
     pruneOpts = [
       "--keep-daily 2"
       "--keep-weekly 1"
@@ -38,17 +48,17 @@ import ./make-test-python.nix (
         {
           services.restic.backups = {
             remotebackup = {
-              inherit passwordFile paths pruneOpts backupPrepareCommand backupCleanupCommand;
+              inherit passwordFile paths exclude pruneOpts backupPrepareCommand backupCleanupCommand;
               repository = remoteRepository;
               initialize = true;
             };
             remote-from-file-backup = {
-              inherit passwordFile paths pruneOpts;
+              inherit passwordFile paths exclude pruneOpts;
               initialize = true;
               repositoryFile = pkgs.writeText "repositoryFile" remoteFromFileRepository;
             };
             rclonebackup = {
-              inherit passwordFile paths pruneOpts;
+              inherit passwordFile paths exclude pruneOpts;
               initialize = true;
               repository = rcloneRepository;
               rcloneConfig = {
@@ -94,16 +104,21 @@ import ./make-test-python.nix (
       )
       server.succeed(
           # set up
-          "mkdir -p /opt",
-          "touch /opt/some_file",
+          "cp -rT ${testDir} /opt",
+          "touch /opt/excluded_file_1 /opt/excluded_file_2",
           "mkdir -p /tmp/restic-rclone-backup",
 
           # test that remotebackup runs custom commands and produces a snapshot
           "timedatectl set-time '2016-12-13 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /tmp/backupCleanupCommand",
           '${pkgs.restic}/bin/restic -r ${remoteRepository} -p ${passwordFile} snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
 
+          # test that restoring that snapshot produces the same directory
+          "mkdir /tmp/restore-1",
+          "${pkgs.restic}/bin/restic -r ${remoteRepository} -p ${passwordFile} restore latest -t /tmp/restore-1",
+          "diff -ru ${testDir} /tmp/restore-1/opt",
+
           # test that remote-from-file-backup produces a snapshot
           "systemctl start restic-backups-remote-from-file-backup.service",
           '${pkgs.restic}/bin/restic -r ${remoteFromFileRepository} -p ${passwordFile} snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
@@ -120,27 +135,27 @@ import ./make-test-python.nix (
           # test that we can create four snapshots in remotebackup and rclonebackup
           "timedatectl set-time '2017-12-13 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /tmp/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           "timedatectl set-time '2018-12-13 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /tmp/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           "timedatectl set-time '2018-12-14 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /tmp/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           "timedatectl set-time '2018-12-15 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /tmp/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           "timedatectl set-time '2018-12-16 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /tmp/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           '${pkgs.restic}/bin/restic -r ${remoteRepository} -p ${passwordFile} snapshots --json | ${pkgs.jq}/bin/jq "length | . == 4"',
diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix
index 039e6bdd9d5ab..94e269ff37bb8 100644
--- a/nixos/tests/systemd-boot.nix
+++ b/nixos/tests/systemd-boot.nix
@@ -101,13 +101,13 @@ in
       # Replace version inside sd-boot with something older. See magic[] string in systemd src/boot/efi/boot.c
       machine.succeed(
           """
-        find /boot -iname '*.efi' -print0 | \
+        find /boot -iname '*boot*.efi' -print0 | \
         xargs -0 -I '{}' sed -i 's/#### LoaderInfo: systemd-boot .* ####/#### LoaderInfo: systemd-boot 000.0-1-notnixos ####/' '{}'
       """
       )
 
       output = machine.succeed("/run/current-system/bin/switch-to-configuration boot")
-      assert "updating systemd-boot from (000.0-1-notnixos) to " in output
+      assert "updating systemd-boot from 000.0-1-notnixos to " in output
     '';
   };