diff options
Diffstat (limited to 'nixos')
43 files changed, 1257 insertions, 89 deletions
diff --git a/nixos/doc/manual/configuration/profiles.chapter.md b/nixos/doc/manual/configuration/profiles.chapter.md index 9f1f48f742ac5..9f6c11b0d59d5 100644 --- a/nixos/doc/manual/configuration/profiles.chapter.md +++ b/nixos/doc/manual/configuration/profiles.chapter.md @@ -29,6 +29,7 @@ profiles/graphical.section.md profiles/hardened.section.md profiles/headless.section.md profiles/installation-device.section.md +profiles/perlless.section.md profiles/minimal.section.md profiles/qemu-guest.section.md ``` diff --git a/nixos/doc/manual/configuration/profiles/perlless.section.md b/nixos/doc/manual/configuration/profiles/perlless.section.md new file mode 100644 index 0000000000000..bf055971cfc42 --- /dev/null +++ b/nixos/doc/manual/configuration/profiles/perlless.section.md @@ -0,0 +1,11 @@ +# Perlless {#sec-perlless} + +::: {.warning} +If you enable this profile, you will NOT be able to switch to a new +configuration and thus you will not be able to rebuild your system with +nixos-rebuild! +::: + +Render your system completely perlless (i.e. without the perl interpreter). This +includes a mechanism so that your build fails if it contains a Nix store path +that references the string "perl". diff --git a/nixos/doc/manual/configuration/user-mgmt.chapter.md b/nixos/doc/manual/configuration/user-mgmt.chapter.md index b35b38f6e964a..71d61ce4c641b 100644 --- a/nixos/doc/manual/configuration/user-mgmt.chapter.md +++ b/nixos/doc/manual/configuration/user-mgmt.chapter.md @@ -89,3 +89,18 @@ A user can be deleted using `userdel`: The flag `-r` deletes the user's home directory. Accounts can be modified using `usermod`. Unix groups can be managed using `groupadd`, `groupmod` and `groupdel`. + +## Create users and groups with `systemd-sysusers` {#sec-systemd-sysusers} + +::: {.note} +This is experimental. +::: + +Instead of using a custom perl script to create users and groups, you can use +systemd-sysusers: + +```nix +systemd.sysusers.enable = true; +``` + +The primary benefit of this is to remove a dependency on perl. diff --git a/nixos/doc/manual/development/etc-overlay.section.md b/nixos/doc/manual/development/etc-overlay.section.md new file mode 100644 index 0000000000000..e6f6d8d4ca1ef --- /dev/null +++ b/nixos/doc/manual/development/etc-overlay.section.md @@ -0,0 +1,36 @@ +# `/etc` via overlay filesystem {#sec-etc-overlay} + +::: {.note} +This is experimental and requires a kernel version >= 6.6 because it uses +new overlay features and relies on the new mount API. +::: + +Instead of using a custom perl script to activate `/etc`, you activate it via an +overlay filesystem: + +```nix +system.etc.overlay.enable = true; +``` + +Using an overlay has two benefits: + +1. it removes a dependency on perl +2. it makes activation faster (up to a few seconds) + +By default, the `/etc` overlay is mounted writable (i.e. there is a writable +upper layer). However, you can also mount `/etc` immutably (i.e. read-only) by +setting: + +```nix +system.etc.overlay.mutable = false; +``` + +The overlay is atomically replaced during system switch. However, files that +have been modified will NOT be overwritten. This is the biggest change compared +to the perl-based system. + +If you manually make changes to `/etc` on your system and then switch to a new +configuration where `system.etc.overlay.mutable = false;`, you will not be able +to see the previously made changes in `/etc` anymore. However the changes are +not completely gone, they are still in the upperdir of the previous overlay in +`/.rw-etc/upper`. diff --git a/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md index 5d17a9c98514c..28c06f999dac2 100644 --- a/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md +++ b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md @@ -56,4 +56,5 @@ explained in the next sections. unit-handling.section.md activation-script.section.md non-switchable-systems.section.md +etc-overlay.section.md ``` diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index 86d3da934dc44..2795323587cba 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -18,6 +18,22 @@ In addition to numerous new and upgraded packages, this release has the followin - Julia environments can now be built with arbitrary packages from the ecosystem using the `.withPackages` function. For example: `julia.withPackages ["Plots"]`. +- A new option `systemd.sysusers.enable` was added. If enabled, users and + groups are created with systemd-sysusers instead of with a custom perl script. + +- A new option `system.etc.overlay.enable` was added. If enabled, `/etc` is + mounted via an overlayfs instead of being created by a custom perl script. + +- It is now possible to have a completely perlless system (i.e. a system + without perl). Previously, the NixOS activation depended on two perl scripts + which can now be replaced via an opt-in mechanism. To make your system + perlless, you can use the new perlless profile: + ``` + { modulesPath, ... }: { + imports = [ "${modulesPath}/profiles/perlless.nix" ]; + } + ``` + ## New Services {#sec-release-24.05-new-services} <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> @@ -130,6 +146,13 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m `CONFIG_FILE_NAME` includes `bpf_pinning`, `ematch_map`, `group`, `nl_protos`, `rt_dsfield`, `rt_protos`, `rt_realms`, `rt_scopes`, and `rt_tables`. +- `netbox` was updated to v3.7. `services.netbox.package` still defaults + to v3.6 if `stateVersion` is earlier than 24.05. Refer to upstream's breaking + changes [for + v3.7.0](https://github.com/netbox-community/netbox/releases/tag/v3.7.0) and + upgrade NetBox by changing `services.netbox.package`. Database migrations + will be run automatically. + - The executable file names for `firefox-devedition`, `firefox-beta`, `firefox-esr` now matches their package names, which is consistent with the `firefox-*-bin` packages. The desktop entries are also updated so that you can have multiple editions of firefox in your app launcher. - switch-to-configuration does not directly call systemd-tmpfiles anymore. diff --git a/nixos/modules/config/shells-environment.nix b/nixos/modules/config/shells-environment.nix index bc6583442edf2..a8476bd2aaedd 100644 --- a/nixos/modules/config/shells-environment.nix +++ b/nixos/modules/config/shells-environment.nix @@ -214,7 +214,8 @@ in '' # Create the required /bin/sh symlink; otherwise lots of things # (notably the system() function) won't work. - mkdir -m 0755 -p /bin + mkdir -p /bin + chmod 0755 /bin ln -sfn "${cfg.binsh}" /bin/.sh.tmp mv /bin/.sh.tmp /bin/sh # atomically replace /bin/sh ''; diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 2aed620eb154c..967ad0846d75b 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -685,7 +685,7 @@ in { shadow.gid = ids.gids.shadow; }; - system.activationScripts.users = { + system.activationScripts.users = if !config.systemd.sysusers.enable then { supportsDryActivation = true; text = '' install -m 0700 -d /root @@ -694,7 +694,7 @@ in { ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \ -w ${./update-users-groups.pl} ${spec} ''; - }; + } else ""; # keep around for backwards compatibility system.activationScripts.update-lingering = let lingerDir = "/var/lib/systemd/linger"; @@ -711,7 +711,9 @@ in { ''; # Warn about user accounts with deprecated password hashing schemes - system.activationScripts.hashes = { + # This does not work when the users and groups are created by + # systemd-sysusers because the users are created too late then. + system.activationScripts.hashes = if !config.systemd.sysusers.enable then { deps = [ "users" ]; text = '' users=() @@ -729,7 +731,7 @@ in { printf ' - %s\n' "''${users[@]}" fi ''; - }; + } else ""; # keep around for backwards compatibility # for backwards compatibility system.activationScripts.groups = stringAfter [ "users" ] ""; diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7c06d67eb0389..2552ca6fa0f54 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1468,6 +1468,7 @@ ./system/boot/stratisroot.nix ./system/boot/modprobe.nix ./system/boot/networkd.nix + ./system/boot/uki.nix ./system/boot/unl0kr.nix ./system/boot/plymouth.nix ./system/boot/resolved.nix @@ -1488,6 +1489,7 @@ ./system/boot/systemd/repart.nix ./system/boot/systemd/shutdown.nix ./system/boot/systemd/sysupdate.nix + ./system/boot/systemd/sysusers.nix ./system/boot/systemd/tmpfiles.nix ./system/boot/systemd/user.nix ./system/boot/systemd/userdbd.nix diff --git a/nixos/modules/profiles/perlless.nix b/nixos/modules/profiles/perlless.nix new file mode 100644 index 0000000000000..90abd14f077e4 --- /dev/null +++ b/nixos/modules/profiles/perlless.nix @@ -0,0 +1,31 @@ +# WARNING: If you enable this profile, you will NOT be able to switch to a new +# configuration and thus you will not be able to rebuild your system with +# nixos-rebuild! + +{ lib, ... }: + +{ + + # Disable switching to a new configuration. This is not a necessary + # limitation of a perlless system but just a current one. In the future, + # perlless switching might be possible. + system.switch.enable = lib.mkDefault false; + + # Remove perl from activation + boot.initrd.systemd.enable = lib.mkDefault true; + system.etc.overlay.enable = lib.mkDefault true; + systemd.sysusers.enable = lib.mkDefault true; + + # Random perl remnants + system.disableInstallerTools = lib.mkDefault true; + programs.less.lessopen = lib.mkDefault null; + programs.command-not-found.enable = lib.mkDefault false; + boot.enableContainers = lib.mkDefault false; + environment.defaultPackages = lib.mkDefault [ ]; + documentation.info.enable = lib.mkDefault false; + + # Check that the system does not contain a Nix store path that contains the + # string "perl". + system.forbiddenDependenciesRegex = "perl"; + +} diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix index c39a3c8d509be..0c1461709c22b 100644 --- a/nixos/modules/programs/ssh.nix +++ b/nixos/modules/programs/ssh.nix @@ -12,6 +12,7 @@ let '' #! ${pkgs.runtimeShell} -e export DISPLAY="$(systemctl --user show-environment | ${pkgs.gnused}/bin/sed 's/^DISPLAY=\(.*\)/\1/; t; d')" + export XAUTHORITY="$(systemctl --user show-environment | ${pkgs.gnused}/bin/sed 's/^XAUTHORITY=\(.*\)/\1/; t; d')" export WAYLAND_DISPLAY="$(systemctl --user show-environment | ${pkgs.gnused}/bin/sed 's/^WAYLAND_DISPLAY=\(.*\)/\1/; t; d')" exec ${cfg.askPassword} "$@" ''; diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix index c86cb81e5df47..9f702b17937cf 100644 --- a/nixos/modules/services/continuous-integration/buildbot/master.nix +++ b/nixos/modules/services/continuous-integration/buildbot/master.nix @@ -267,8 +267,7 @@ in { systemd.services.buildbot-master = { description = "Buildbot Continuous Integration Server."; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; + after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; path = cfg.packages ++ cfg.pythonPackages python.pkgs; environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ package ])}/${python.sitePackages}"; diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix index 79c8fec752521..25c7017a1d258 100644 --- a/nixos/modules/services/mail/dovecot.nix +++ b/nixos/modules/services/mail/dovecot.nix @@ -119,10 +119,9 @@ let '' plugin { sieve_plugins = ${concatStringsSep " " cfg.sieve.plugins} + sieve_extensions = ${concatStringsSep " " (map (el: "+${el}") cfg.sieve.extensions)} + sieve_global_extensions = ${concatStringsSep " " (map (el: "+${el}") cfg.sieve.globalExtensions)} '' - (optionalString (cfg.sieve.extensions != []) ''sieve_extensions = ${concatMapStringsSep " " (el: "+${el}") cfg.sieve.extensions}'') - (optionalString (cfg.sieve.globalExtensions != []) ''sieve_global_extensions = ${concatMapStringsSep " " (el: "+${el}") cfg.sieve.globalExtensions}'') - (optionalString (cfg.imapsieve.mailbox != []) '' ${ concatStringsSep "\n" (flatten (imap1 ( diff --git a/nixos/modules/services/misc/moonraker.nix b/nixos/modules/services/misc/moonraker.nix index 750dca9d03736..4e419aafa990b 100644 --- a/nixos/modules/services/misc/moonraker.nix +++ b/nixos/modules/services/misc/moonraker.nix @@ -103,7 +103,7 @@ in { config = mkIf cfg.enable { warnings = [] - ++ optional (cfg.settings ? update_manager) + ++ optional (cfg.settings.update_manager.enable_system_updates or false) ''Enabling update_manager is not supported on NixOS and will lead to non-removable warnings in some clients.'' ++ optional (cfg.configDir != null) '' diff --git a/nixos/modules/services/misc/nix-gc.nix b/nixos/modules/services/misc/nix-gc.nix index 97596d28cd89b..de6bd76c7eb9d 100644 --- a/nixos/modules/services/misc/nix-gc.nix +++ b/nixos/modules/services/misc/nix-gc.nix @@ -1,7 +1,5 @@ { config, lib, ... }: -with lib; - let cfg = config.nix.gc; in @@ -14,14 +12,14 @@ in nix.gc = { - automatic = mkOption { + automatic = lib.mkOption { default = false; - type = types.bool; + type = lib.types.bool; description = lib.mdDoc "Automatically run the garbage collector at a specific time."; }; - dates = mkOption { - type = types.str; + dates = lib.mkOption { + type = lib.types.singleLineStr; default = "03:15"; example = "weekly"; description = lib.mdDoc '' @@ -33,9 +31,9 @@ in ''; }; - randomizedDelaySec = mkOption { + randomizedDelaySec = lib.mkOption { default = "0"; - type = types.str; + type = lib.types.singleLineStr; example = "45min"; description = lib.mdDoc '' Add a randomized delay before each garbage collection. @@ -45,9 +43,9 @@ in ''; }; - persistent = mkOption { + persistent = lib.mkOption { default = true; - type = types.bool; + type = lib.types.bool; example = false; description = lib.mdDoc '' Takes a boolean argument. If true, the time when the service @@ -61,10 +59,10 @@ in ''; }; - options = mkOption { + options = lib.mkOption { default = ""; example = "--max-freed $((64 * 1024**3))"; - type = types.str; + type = lib.types.singleLineStr; description = lib.mdDoc '' Options given to {file}`nix-collect-garbage` when the garbage collector is run automatically. @@ -89,7 +87,8 @@ in systemd.services.nix-gc = lib.mkIf config.nix.enable { description = "Nix Garbage Collector"; script = "exec ${config.nix.package.out}/bin/nix-collect-garbage ${cfg.options}"; - startAt = optional cfg.automatic cfg.dates; + serviceConfig.Type = "oneshot"; + startAt = lib.optional cfg.automatic cfg.dates; }; systemd.timers.nix-gc = lib.mkIf cfg.automatic { diff --git a/nixos/modules/services/misc/ollama.nix b/nixos/modules/services/misc/ollama.nix index 9794bbbec464c..d9359d2b5cd44 100644 --- a/nixos/modules/services/misc/ollama.nix +++ b/nixos/modules/services/misc/ollama.nix @@ -9,6 +9,13 @@ in { enable = lib.mkEnableOption ( lib.mdDoc "Server for local large language models" ); + listenAddress = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1:11434"; + description = lib.mdDoc '' + Specifies the bind address on which the ollama server HTTP interface listens. + ''; + }; package = lib.mkPackageOption pkgs "ollama" { }; }; }; @@ -23,6 +30,7 @@ in { environment = { HOME = "%S/ollama"; OLLAMA_MODELS = "%S/ollama/models"; + OLLAMA_HOST = cfg.listenAddress; }; serviceConfig = { ExecStart = "${lib.getExe cfg.package} serve"; diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py index fec05728b2b6b..b1eebb07686b2 100644 --- a/nixos/modules/services/misc/taskserver/helper-tool.py +++ b/nixos/modules/services/misc/taskserver/helper-tool.py @@ -61,6 +61,10 @@ def run_as_taskd_user(): os.setuid(uid) +def run_as_taskd_group(): + gid = grp.getgrnam(TASKD_GROUP).gr_gid + os.setgid(gid) + def taskd_cmd(cmd, *args, **kwargs): """ Invoke taskd with the specified command with the privileges of the 'taskd' @@ -90,7 +94,7 @@ def certtool_cmd(*args, **kwargs): """ return subprocess.check_output( [CERTTOOL_COMMAND] + list(args), - preexec_fn=lambda: os.umask(0o077), + preexec_fn=run_as_taskd_group, stderr=subprocess.STDOUT, **kwargs ) @@ -156,17 +160,33 @@ def generate_key(org, user): sys.stderr.write(msg.format(user)) return - basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user) - if os.path.exists(basedir): + keysdir = os.path.join(TASKD_DATA_DIR, "keys" ) + orgdir = os.path.join(keysdir , org ) + userdir = os.path.join(orgdir , user ) + if os.path.exists(userdir): raise OSError("Keyfile directory for {} already exists.".format(user)) - privkey = os.path.join(basedir, "private.key") - pubcert = os.path.join(basedir, "public.cert") + privkey = os.path.join(userdir, "private.key") + pubcert = os.path.join(userdir, "public.cert") try: - os.makedirs(basedir, mode=0o700) + # We change the permissions and the owner ship of the base directories + # so that cfg.group and cfg.user could read the directories' contents. + # See also: https://bugs.python.org/issue42367 + for bd in [keysdir, orgdir, userdir]: + # Allow cfg.group, but not others to read the contents of this group + os.makedirs(bd, exist_ok=True) + # not using mode= argument to makedirs intentionally - forcing the + # permissions we want + os.chmod(bd, mode=0o750) + os.chown( + bd, + uid=pwd.getpwnam(TASKD_USER).pw_uid, + gid=grp.getgrnam(TASKD_GROUP).gr_gid, + ) certtool_cmd("-p", "--bits", CERT_BITS, "--outfile", privkey) + os.chmod(privkey, 0o640) template_data = [ "organization = {0}".format(org), @@ -187,7 +207,7 @@ def generate_key(org, user): "--outfile", pubcert ) except: - rmtree(basedir) + rmtree(userdir) raise diff --git a/nixos/modules/services/networking/bird.nix b/nixos/modules/services/networking/bird.nix index 9deeb7694d2ac..e25f5c7b03794 100644 --- a/nixos/modules/services/networking/bird.nix +++ b/nixos/modules/services/networking/bird.nix @@ -18,6 +18,13 @@ in <http://bird.network.cz/> ''; }; + autoReload = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether bird2 should be automatically reloaded when the configuration changes. + ''; + }; checkConfig = mkOption { type = types.bool; default = true; @@ -68,7 +75,7 @@ in systemd.services.bird2 = { description = "BIRD Internet Routing Daemon"; wantedBy = [ "multi-user.target" ]; - reloadTriggers = [ config.environment.etc."bird/bird2.conf".source ]; + reloadTriggers = lib.optional cfg.autoReload config.environment.etc."bird/bird2.conf".source; serviceConfig = { Type = "forking"; Restart = "on-failure"; diff --git a/nixos/modules/services/networking/keepalived/default.nix b/nixos/modules/services/networking/keepalived/default.nix index 429a47c3962c6..599dfd52e271f 100644 --- a/nixos/modules/services/networking/keepalived/default.nix +++ b/nixos/modules/services/networking/keepalived/default.nix @@ -59,9 +59,11 @@ let ${optionalString i.vmacXmitBase "vmac_xmit_base"} ${optionalString (i.unicastSrcIp != null) "unicast_src_ip ${i.unicastSrcIp}"} - unicast_peer { - ${concatStringsSep "\n" i.unicastPeers} - } + ${optionalString (builtins.length i.unicastPeers > 0) '' + unicast_peer { + ${concatStringsSep "\n" i.unicastPeers} + } + ''} virtual_ipaddress { ${concatMapStringsSep "\n" virtualIpLine i.virtualIps} @@ -138,6 +140,7 @@ let in { + meta.maintainers = [ lib.maintainers.raitobezarius ]; options = { services.keepalived = { diff --git a/nixos/modules/services/security/clamav.nix b/nixos/modules/services/security/clamav.nix index d3164373ec01f..4480c0cae60c9 100644 --- a/nixos/modules/services/security/clamav.nix +++ b/nixos/modules/services/security/clamav.nix @@ -196,6 +196,7 @@ in systemd.services.clamav-freshclam = mkIf cfg.updater.enable { description = "ClamAV virus database updater (freshclam)"; restartTriggers = [ freshclamConfigFile ]; + requires = [ "network-online.target" ]; after = [ "network-online.target" ]; serviceConfig = { @@ -243,6 +244,7 @@ in systemd.services.clamav-fangfrisch = mkIf cfg.fangfrisch.enable { description = "ClamAV virus database updater (fangfrisch)"; restartTriggers = [ fangfrischConfigFile ]; + requires = [ "network-online.target" ]; after = [ "network-online.target" "clamav-fangfrisch-init.service" ]; serviceConfig = { diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix index b47ebc92f93a8..e8f8b48d0337f 100644 --- a/nixos/modules/services/system/dbus.nix +++ b/nixos/modules/services/system/dbus.nix @@ -95,6 +95,7 @@ in uid = config.ids.uids.messagebus; description = "D-Bus system message bus daemon user"; home = homeDir; + homeMode = "0755"; group = "messagebus"; }; diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix index 72ec578146a76..d034f3234a2bd 100644 --- a/nixos/modules/services/web-apps/netbox.nix +++ b/nixos/modules/services/web-apps/netbox.nix @@ -75,13 +75,17 @@ in { package = lib.mkOption { type = lib.types.package; default = - if lib.versionAtLeast config.system.stateVersion "23.11" + if lib.versionAtLeast config.system.stateVersion "24.05" + then pkgs.netbox_3_7 + else if lib.versionAtLeast config.system.stateVersion "23.11" then pkgs.netbox_3_6 else if lib.versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox_3_5 else pkgs.netbox_3_3; defaultText = lib.literalExpression '' - if lib.versionAtLeast config.system.stateVersion "23.11" + if lib.versionAtLeast config.system.stateVersion "24.05" + then pkgs.netbox_3_7 + else if lib.versionAtLeast config.system.stateVersion "23.11" then pkgs.netbox_3_6 else if lib.versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox_3_5 @@ -306,12 +310,13 @@ in { ${pkg}/bin/netbox trace_paths --no-input ${pkg}/bin/netbox collectstatic --no-input ${pkg}/bin/netbox remove_stale_contenttypes --no-input - # TODO: remove the condition when we remove netbox_3_3 - ${lib.optionalString - (lib.versionAtLeast cfg.package.version "3.5.0") - "${pkg}/bin/netbox reindex --lazy"} + ${pkg}/bin/netbox reindex --lazy ${pkg}/bin/netbox clearsessions - ${pkg}/bin/netbox clearcache + ${lib.optionalString + # The clearcache command was removed in 3.7.0: + # https://github.com/netbox-community/netbox/issues/14458 + (lib.versionOlder cfg.package.version "3.7.0") + "${pkg}/bin/netbox clearcache"} echo "${cfg.package.version}" > "$versionFile" ''; diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix index 3b1d87ccb483e..e545869ca4320 100644 --- a/nixos/modules/services/web-servers/ttyd.nix +++ b/nixos/modules/services/web-servers/ttyd.nix @@ -180,10 +180,11 @@ in # Runs login which needs to be run as root # login: Cannot possibly work without effective root User = "root"; + LoadCredential = lib.optionalString (cfg.passwordFile != null) "TTYD_PASSWORD_FILE:${cfg.passwordFile}"; }; script = if cfg.passwordFile != null then '' - PASSWORD=$(cat ${escapeShellArg cfg.passwordFile}) + PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE") ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ --credential ${escapeShellArg cfg.username}:"$PASSWORD" \ ${pkgs.shadow}/bin/login diff --git a/nixos/modules/system/boot/systemd/sysusers.nix b/nixos/modules/system/boot/systemd/sysusers.nix new file mode 100644 index 0000000000000..c619c2d91eb09 --- /dev/null +++ b/nixos/modules/system/boot/systemd/sysusers.nix @@ -0,0 +1,169 @@ +{ config, lib, pkgs, utils, ... }: + +let + + cfg = config.systemd.sysusers; + userCfg = config.users; + + sysusersConfig = pkgs.writeTextDir "00-nixos.conf" '' + # Type Name ID GECOS Home directory Shell + + # Users + ${lib.concatLines (lib.mapAttrsToList + (username: opts: + let + uid = if opts.uid == null then "-" else toString opts.uid; + in + ''u ${username} ${uid}:${opts.group} "${opts.description}" ${opts.home} ${utils.toShellPath opts.shell}'' + ) + userCfg.users) + } + + # Groups + ${lib.concatLines (lib.mapAttrsToList + (groupname: opts: ''g ${groupname} ${if opts.gid == null then "-" else toString opts.gid}'') userCfg.groups) + } + + # Group membership + ${lib.concatStrings (lib.mapAttrsToList + (groupname: opts: (lib.concatMapStrings (username: "m ${username} ${groupname}\n")) opts.members ) userCfg.groups) + } + ''; + + staticSysusersCredentials = pkgs.runCommand "static-sysusers-credentials" { } '' + mkdir $out; cd $out + ${lib.concatLines ( + (lib.mapAttrsToList + (username: opts: "echo -n '${opts.initialHashedPassword}' > 'passwd.hashed-password.${username}'") + (lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) userCfg.users)) + ++ + (lib.mapAttrsToList + (username: opts: "echo -n '${opts.initialPassword}' > 'passwd.plaintext-password.${username}'") + (lib.filterAttrs (_username: opts: opts.initialPassword != null) userCfg.users)) + ++ + (lib.mapAttrsToList + (username: opts: "cat '${opts.hashedPasswordFile}' > 'passwd.hashed-password.${username}'") + (lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) userCfg.users)) + ) + } + ''; + + staticSysusers = pkgs.runCommand "static-sysusers" + { + nativeBuildInputs = [ pkgs.systemd ]; + } '' + mkdir $out + export CREDENTIALS_DIRECTORY=${staticSysusersCredentials} + systemd-sysusers --root $out ${sysusersConfig}/00-nixos.conf + ''; + +in + +{ + + options = { + + # This module doesn't set it's own user options but reuses the ones from + # users-groups.nix + + systemd.sysusers = { + enable = lib.mkEnableOption (lib.mdDoc "systemd-sysusers") // { + description = lib.mdDoc '' + If enabled, users are created with systemd-sysusers instead of with + the custom `update-users-groups.pl` script. + + Note: This is experimental. + ''; + }; + }; + + }; + + config = lib.mkIf cfg.enable { + + assertions = [ + { + assertion = config.system.activationScripts.users == ""; + message = "system.activationScripts.users has to be empty to use systemd-sysusers"; + } + { + assertion = config.users.mutableUsers -> config.system.etc.overlay.enable; + message = "config.users.mutableUsers requires config.system.etc.overlay.enable."; + } + ]; + + systemd = lib.mkMerge [ + ({ + + # Create home directories, do not create /var/empty even if that's a user's + # home. + tmpfiles.settings.home-directories = lib.mapAttrs' + (username: opts: lib.nameValuePair opts.home { + d = { + mode = opts.homeMode; + user = username; + group = opts.group; + }; + }) + (lib.filterAttrs (_username: opts: opts.home != "/var/empty") userCfg.users); + }) + + (lib.mkIf config.users.mutableUsers { + additionalUpstreamSystemUnits = [ + "systemd-sysusers.service" + ]; + + services.systemd-sysusers = { + # Enable switch-to-configuration to restart the service. + unitConfig.ConditionNeedsUpdate = [ "" ]; + requiredBy = [ "sysinit-reactivation.target" ]; + before = [ "sysinit-reactivation.target" ]; + restartTriggers = [ "${config.environment.etc."sysusers.d".source}" ]; + + serviceConfig = { + LoadCredential = lib.mapAttrsToList + (username: opts: "passwd.hashed-password.${username}:${opts.hashedPasswordFile}") + (lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) userCfg.users); + SetCredential = (lib.mapAttrsToList + (username: opts: "passwd.hashed-password.${username}:${opts.initialHashedPassword}") + (lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) userCfg.users)) + ++ + (lib.mapAttrsToList + (username: opts: "passwd.plaintext-password.${username}:${opts.initialPassword}") + (lib.filterAttrs (_username: opts: opts.initialPassword != null) userCfg.users)) + ; + }; + }; + }) + ]; + + environment.etc = lib.mkMerge [ + (lib.mkIf (!userCfg.mutableUsers) { + "passwd" = { + source = "${staticSysusers}/etc/passwd"; + mode = "0644"; + }; + "group" = { + source = "${staticSysusers}/etc/group"; + mode = "0644"; + }; + "shadow" = { + source = "${staticSysusers}/etc/shadow"; + mode = "0000"; + }; + "gshadow" = { + source = "${staticSysusers}/etc/gshadow"; + mode = "0000"; + }; + }) + + (lib.mkIf userCfg.mutableUsers { + "sysusers.d".source = sysusersConfig; + }) + ]; + + }; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + +} diff --git a/nixos/modules/system/boot/uki.nix b/nixos/modules/system/boot/uki.nix new file mode 100644 index 0000000000000..63c4e0c0e3913 --- /dev/null +++ b/nixos/modules/system/boot/uki.nix @@ -0,0 +1,85 @@ +{ config, lib, pkgs, ... }: + +let + + cfg = config.boot.uki; + + inherit (pkgs.stdenv.hostPlatform) efiArch; + + format = pkgs.formats.ini { }; + ukifyConfig = format.generate "ukify.conf" cfg.settings; + +in + +{ + options = { + + boot.uki = { + name = lib.mkOption { + type = lib.types.str; + description = lib.mdDoc "Name of the UKI"; + }; + + version = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = config.system.image.version; + defaultText = lib.literalExpression "config.system.image.version"; + description = lib.mdDoc "Version of the image or generation the UKI belongs to"; + }; + + settings = lib.mkOption { + type = format.type; + description = lib.mdDoc '' + The configuration settings for ukify. These control what the UKI + contains and how it is built. + ''; + }; + }; + + system.boot.loader.ukiFile = lib.mkOption { + type = lib.types.str; + internal = true; + description = lib.mdDoc "Name of the UKI file"; + }; + + }; + + config = { + + boot.uki.name = lib.mkOptionDefault (if config.system.image.id != null then + config.system.image.id + else + "nixos"); + + boot.uki.settings = lib.mkOptionDefault { + UKI = { + Linux = "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}"; + Initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}"; + Cmdline = "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}"; + Stub = "${pkgs.systemd}/lib/systemd/boot/efi/linux${efiArch}.efi.stub"; + Uname = "${config.boot.kernelPackages.kernel.modDirVersion}"; + OSRelease = "@${config.system.build.etc}/etc/os-release"; + # This is needed for cross compiling. + EFIArch = efiArch; + }; + }; + + system.boot.loader.ukiFile = + let + name = config.boot.uki.name; + version = config.boot.uki.version; + versionInfix = if version != null then "_${version}" else ""; + in + name + versionInfix + ".efi"; + + system.build.uki = pkgs.runCommand config.system.boot.loader.ukiFile { } '' + mkdir -p $out + ${pkgs.buildPackages.systemdUkify}/lib/systemd/ukify build \ + --config=${ukifyConfig} \ + --output="$out/${config.system.boot.loader.ukiFile}" + ''; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + }; +} diff --git a/nixos/modules/system/etc/build-composefs-dump.py b/nixos/modules/system/etc/build-composefs-dump.py new file mode 100644 index 0000000000000..923d40008b63f --- /dev/null +++ b/nixos/modules/system/etc/build-composefs-dump.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 + +"""Build a composefs dump from a Json config + +See the man page of composefs-dump for details about the format: +https://github.com/containers/composefs/blob/main/man/composefs-dump.md + +Ensure to check the file with the check script when you make changes to it: + +./check-build-composefs-dump.sh ./build-composefs_dump.py +""" + +import glob +import json +import os +import sys +from enum import Enum +from pathlib import Path +from typing import Any + +Attrs = dict[str, Any] + + +class FileType(Enum): + """The filetype as defined by the `st_mode` stat field in octal + + You can check the st_mode stat field of a path in Python with + `oct(os.stat("/path/").st_mode)` + """ + + directory = "4" + file = "10" + symlink = "12" + + +class ComposefsPath: + path: str + size: int + filetype: FileType + mode: str + uid: str + gid: str + payload: str + rdev: str = "0" + nlink: int = 1 + mtime: str = "1.0" + content: str = "-" + digest: str = "-" + + def __init__( + self, + attrs: Attrs, + size: int, + filetype: FileType, + mode: str, + payload: str, + path: str | None = None, + ): + if path is None: + path = attrs["target"] + self.path = "/" + path + self.size = size + self.filetype = filetype + self.mode = mode + self.uid = attrs["uid"] + self.gid = attrs["gid"] + self.payload = payload + + def write_line(self) -> str: + line_list = [ + str(self.path), + str(self.size), + f"{self.filetype.value}{self.mode}", + str(self.nlink), + str(self.uid), + str(self.gid), + str(self.rdev), + str(self.mtime), + str(self.payload), + str(self.content), + str(self.digest), + ] + return " ".join(line_list) + + +def eprint(*args, **kwargs) -> None: + print(args, **kwargs, file=sys.stderr) + + +def leading_directories(path: str) -> list[str]: + """Return the leading directories of path + + Given the path "alsa/conf.d/50-pipewire.conf", for example, this function + returns `[ "alsa", "alsa/conf.d" ]`. + """ + parents = list(Path(path).parents) + parents.reverse() + # remove the implicit `.` from the start of a relative path or `/` from an + # absolute path + del parents[0] + return [str(i) for i in parents] + + +def add_leading_directories( + target: str, attrs: Attrs, paths: dict[str, ComposefsPath] +) -> None: + """Add the leading directories of a target path to the composefs paths + + mkcomposefs expects that all leading directories are explicitly listed in + the dump file. Given the path "alsa/conf.d/50-pipewire.conf", for example, + this function adds "alsa" and "alsa/conf.d" to the composefs paths. + """ + path_components = leading_directories(target) + for component in path_components: + composefs_path = ComposefsPath( + attrs, + path=component, + size=4096, + filetype=FileType.directory, + mode="0755", + payload="-", + ) + paths[component] = composefs_path + + +def main() -> None: + """Build a composefs dump from a Json config + + This config describes the files that the final composefs image is supposed + to contain. + """ + config_file = sys.argv[1] + if not config_file: + eprint("No config file was supplied.") + sys.exit(1) + + with open(config_file, "rb") as f: + config = json.load(f) + + if not config: + eprint("Config is empty.") + sys.exit(1) + + eprint("Building composefs dump...") + + paths: dict[str, ComposefsPath] = {} + for attrs in config: + target = attrs["target"] + source = attrs["source"] + mode = attrs["mode"] + + if "*" in source: # Path with globbing + glob_sources = glob.glob(source) + for glob_source in glob_sources: + basename = os.path.basename(glob_source) + glob_target = f"{target}/{basename}" + + composefs_path = ComposefsPath( + attrs, + path=glob_target, + size=100, + filetype=FileType.symlink, + mode="0777", + payload=glob_source, + ) + + paths[glob_target] = composefs_path + add_leading_directories(glob_target, attrs, paths) + else: # Without globbing + if mode == "symlink": + composefs_path = ComposefsPath( + attrs, + # A high approximation of the size of a symlink + size=100, + filetype=FileType.symlink, + mode="0777", + payload=source, + ) + else: + if os.path.isdir(source): + composefs_path = ComposefsPath( + attrs, + size=4096, + filetype=FileType.directory, + mode=mode, + payload=source, + ) + else: + composefs_path = ComposefsPath( + attrs, + size=os.stat(source).st_size, + filetype=FileType.file, + mode=mode, + payload=target, + ) + paths[target] = composefs_path + add_leading_directories(target, attrs, paths) + + composefs_dump = ["/ 4096 40755 1 0 0 0 0.0 - - -"] # Root directory + for key in sorted(paths): + composefs_path = paths[key] + eprint(composefs_path.path) + composefs_dump.append(composefs_path.write_line()) + + print("\n".join(composefs_dump)) + + +if __name__ == "__main__": + main() diff --git a/nixos/modules/system/etc/check-build-composefs-dump.sh b/nixos/modules/system/etc/check-build-composefs-dump.sh new file mode 100755 index 0000000000000..da61651d1a5d6 --- /dev/null +++ b/nixos/modules/system/etc/check-build-composefs-dump.sh @@ -0,0 +1,8 @@ +#! /usr/bin/env nix-shell +#! nix-shell -i bash -p black ruff mypy + +file=$1 + +black --check --diff $file +ruff --line-length 88 $file +mypy --strict $file diff --git a/nixos/modules/system/etc/etc-activation.nix b/nixos/modules/system/etc/etc-activation.nix index 7801049501860..f47fd771c6592 100644 --- a/nixos/modules/system/etc/etc-activation.nix +++ b/nixos/modules/system/etc/etc-activation.nix @@ -1,12 +1,96 @@ { config, lib, ... }: -let - inherit (lib) stringAfter; -in { + +{ imports = [ ./etc.nix ]; - config = { - system.activationScripts.etc = - stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands; - }; + config = lib.mkMerge [ + + { + system.activationScripts.etc = + lib.stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands; + } + + (lib.mkIf config.system.etc.overlay.enable { + + assertions = [ + { + assertion = config.boot.initrd.systemd.enable; + message = "`system.etc.overlay.enable` requires `boot.initrd.systemd.enable`"; + } + { + assertion = (!config.system.etc.overlay.mutable) -> config.systemd.sysusers.enable; + message = "`system.etc.overlay.mutable = false` requires `systemd.sysusers.enable`"; + } + { + assertion = lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.6"; + message = "`system.etc.overlay.enable requires a newer kernel, at least version 6.6"; + } + { + assertion = config.systemd.sysusers.enable -> (config.users.mutableUsers == config.system.etc.overlay.mutable); + message = '' + When using systemd-sysusers and mounting `/etc` via an overlay, users + can only be mutable when `/etc` is mutable and vice versa. + ''; + } + ]; + + boot.initrd.availableKernelModules = [ "loop" "erofs" "overlay" ]; + + boot.initrd.systemd = { + mounts = [ + { + where = "/run/etc-metadata"; + what = "/sysroot${config.system.build.etcMetadataImage}"; + type = "erofs"; + options = "loop"; + unitConfig.RequiresMountsFor = [ + "/sysroot/nix/store" + ]; + } + { + where = "/sysroot/etc"; + what = "overlay"; + type = "overlay"; + options = lib.concatStringsSep "," ([ + "relatime" + "redirect_dir=on" + "metacopy=on" + "lowerdir=/run/etc-metadata::/sysroot${config.system.build.etcBasedir}" + ] ++ lib.optionals config.system.etc.overlay.mutable [ + "rw" + "upperdir=/sysroot/.rw-etc/upper" + "workdir=/sysroot/.rw-etc/work" + ] ++ lib.optionals (!config.system.etc.overlay.mutable) [ + "ro" + ]); + wantedBy = [ "initrd-fs.target" ]; + before = [ "initrd-fs.target" ]; + requires = lib.mkIf config.system.etc.overlay.mutable [ "rw-etc.service" ]; + after = lib.mkIf config.system.etc.overlay.mutable [ "rw-etc.service" ]; + unitConfig.RequiresMountsFor = [ + "/sysroot/nix/store" + "/run/etc-metadata" + ]; + } + ]; + services = lib.mkIf config.system.etc.overlay.mutable { + rw-etc = { + unitConfig = { + DefaultDependencies = false; + RequiresMountsFor = "/sysroot"; + }; + serviceConfig = { + Type = "oneshot"; + ExecStart = '' + /bin/mkdir -p -m 0755 /sysroot/.rw-etc/upper /sysroot/.rw-etc/work + ''; + }; + }; + }; + }; + + }) + + ]; } diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix index ea61e7384e60c..baf37ba6def34 100644 --- a/nixos/modules/system/etc/etc.nix +++ b/nixos/modules/system/etc/etc.nix @@ -62,6 +62,16 @@ let ]) etc'} ''; + etcHardlinks = filter (f: f.mode != "symlink") etc'; + + build-composefs-dump = pkgs.runCommand "build-composefs-dump.py" + { + buildInputs = [ pkgs.python3 ]; + } '' + install ${./build-composefs-dump.py} $out + patchShebangs --host $out + ''; + in { @@ -72,6 +82,30 @@ in options = { + system.etc.overlay = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Mount `/etc` as an overlayfs instead of generating it via a perl script. + + Note: This is currently experimental. Only enable this option if you're + confident that you can recover your system if it breaks. + ''; + }; + + mutable = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to mount `/etc` mutably (i.e. read-write) or immutably (i.e. read-only). + + If this is false, only the immutable lowerdir is mounted. If it is + true, a writable upperdir is mounted on top. + ''; + }; + }; + environment.etc = mkOption { default = {}; example = literalExpression '' @@ -190,12 +224,84 @@ in config = { system.build.etc = etc; - system.build.etcActivationCommands = - '' - # Set up the statically computed bits of /etc. - echo "setting up /etc..." - ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc + system.build.etcActivationCommands = let + etcOverlayOptions = lib.concatStringsSep "," ([ + "relatime" + "redirect_dir=on" + "metacopy=on" + ] ++ lib.optionals config.system.etc.overlay.mutable [ + "upperdir=/.rw-etc/upper" + "workdir=/.rw-etc/work" + ]); + in if config.system.etc.overlay.enable then '' + # This script atomically remounts /etc when switching configuration. On a (re-)boot + # this should not run because /etc is mounted via a systemd mount unit + # instead. To a large extent this mimics what composefs does. Because + # it's relatively simple, however, we avoid the composefs dependency. + if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]]; then + echo "remounting /etc..." + + tmpMetadataMount=$(mktemp --directory) + mount --type erofs ${config.system.build.etcMetadataImage} $tmpMetadataMount + + # Mount the new /etc overlay to a temporary private mount. + # This needs the indirection via a private bind mount because you + # cannot move shared mounts. + tmpEtcMount=$(mktemp --directory) + mount --bind --make-private $tmpEtcMount $tmpEtcMount + mount --type overlay overlay \ + --options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \ + $tmpEtcMount + + # Move the new temporary /etc mount underneath the current /etc mount. + # + # This should eventually use util-linux to perform this move beneath, + # however, this functionality is not yet in util-linux. See this + # tracking issue: https://github.com/util-linux/util-linux/issues/2604 + ${pkgs.move-mount-beneath}/bin/move-mount --move --beneath $tmpEtcMount /etc + + # Unmount the top /etc mount to atomically reveal the new mount. + umount /etc + + fi + '' else '' + # Set up the statically computed bits of /etc. + echo "setting up /etc..." + ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc + ''; + + system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } '' + set -euo pipefail + + makeEtcEntry() { + src="$1" + target="$2" + + mkdir -p "$out/$(dirname "$target")" + cp "$src" "$out/$target" + } + + mkdir -p "$out" + ${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [ + "makeEtcEntry" + # Force local source paths to be added to the store + "${etcEntry.source}" + etcEntry.target + ]) etcHardlinks} + ''; + + system.build.etcMetadataImage = + let + etcJson = pkgs.writeText "etc-json" (builtins.toJSON etc'); + etcDump = pkgs.runCommand "etc-dump" { } "${build-composefs-dump} ${etcJson} > $out"; + in + pkgs.runCommand "etc-metadata.erofs" { + nativeBuildInputs = [ pkgs.composefs pkgs.erofs-utils ]; + } '' + mkcomposefs --from-file ${etcDump} $out + fsck.erofs $out ''; + }; } diff --git a/nixos/modules/tasks/auto-upgrade.nix b/nixos/modules/tasks/auto-upgrade.nix index 29e3e313336f5..22311871274b9 100644 --- a/nixos/modules/tasks/auto-upgrade.nix +++ b/nixos/modules/tasks/auto-upgrade.nix @@ -109,6 +109,17 @@ in { ''; }; + fixedRandomDelay = mkOption { + default = false; + type = types.bool; + example = true; + description = lib.mdDoc '' + Make the randomized delay consistent between runs. + This reduces the jitter between automatic upgrades. + See {option}`randomizedDelaySec` for configuring the randomized delay. + ''; + }; + rebootWindow = mkOption { description = lib.mdDoc '' Define a lower and upper time value (in HH:MM format) which @@ -253,6 +264,7 @@ in { systemd.timers.nixos-upgrade = { timerConfig = { RandomizedDelaySec = cfg.randomizedDelaySec; + FixedRandomDelay = cfg.fixedRandomDelay; Persistent = cfg.persistent; }; }; diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix index 9ee77cd79a9b1..6aa718c1975d7 100644 --- a/nixos/modules/testing/test-instrumentation.nix +++ b/nixos/modules/testing/test-instrumentation.nix @@ -207,7 +207,10 @@ in networking.usePredictableInterfaceNames = false; # Make it easy to log in as root when running the test interactively. - users.users.root.initialHashedPassword = mkOverride 150 ""; + # This needs to be a file because of a quirk in systemd credentials, + # where you cannot specify an empty string as a value. systemd-sysusers + # uses credentials to set passwords on users. + users.users.root.hashedPasswordFile = mkOverride 150 "${pkgs.writeText "hashed-password.root" ""}"; services.xserver.displayManager.job.logToJournal = true; diff --git a/nixos/tests/activation/etc-overlay-immutable.nix b/nixos/tests/activation/etc-overlay-immutable.nix new file mode 100644 index 0000000000000..70c3623b929c5 --- /dev/null +++ b/nixos/tests/activation/etc-overlay-immutable.nix @@ -0,0 +1,30 @@ +{ lib, ... }: { + + name = "activation-etc-overlay-immutable"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { pkgs, ... }: { + system.etc.overlay.enable = true; + system.etc.overlay.mutable = false; + + # Prerequisites + systemd.sysusers.enable = true; + users.mutableUsers = false; + boot.initrd.systemd.enable = true; + boot.kernelPackages = pkgs.linuxPackages_latest; + + specialisation.new-generation.configuration = { + environment.etc."newgen".text = "newgen"; + }; + }; + + testScript = '' + machine.succeed("findmnt --kernel --type overlay /etc") + machine.fail("stat /etc/newgen") + + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch") + + assert machine.succeed("cat /etc/newgen") == "newgen" + ''; +} diff --git a/nixos/tests/activation/etc-overlay-mutable.nix b/nixos/tests/activation/etc-overlay-mutable.nix new file mode 100644 index 0000000000000..cfe7604fceb84 --- /dev/null +++ b/nixos/tests/activation/etc-overlay-mutable.nix @@ -0,0 +1,30 @@ +{ lib, ... }: { + + name = "activation-etc-overlay-mutable"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { pkgs, ... }: { + system.etc.overlay.enable = true; + system.etc.overlay.mutable = true; + + # Prerequisites + boot.initrd.systemd.enable = true; + boot.kernelPackages = pkgs.linuxPackages_latest; + + specialisation.new-generation.configuration = { + environment.etc."newgen".text = "newgen"; + }; + }; + + testScript = '' + machine.succeed("findmnt --kernel --type overlay /etc") + machine.fail("stat /etc/newgen") + machine.succeed("echo -n 'mutable' > /etc/mutable") + + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch") + + assert machine.succeed("cat /etc/newgen") == "newgen" + assert machine.succeed("cat /etc/mutable") == "mutable" + ''; +} diff --git a/nixos/tests/activation/perlless.nix b/nixos/tests/activation/perlless.nix new file mode 100644 index 0000000000000..4d784b4542f45 --- /dev/null +++ b/nixos/tests/activation/perlless.nix @@ -0,0 +1,24 @@ +{ lib, ... }: + +{ + + name = "activation-perlless"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { pkgs, modulesPath, ... }: { + imports = [ "${modulesPath}/profiles/perlless.nix" ]; + + boot.kernelPackages = pkgs.linuxPackages_latest; + + virtualisation.mountHostNixStore = false; + virtualisation.useNixStoreImage = true; + }; + + testScript = '' + perl_store_paths = machine.succeed("ls /nix/store | grep perl || true") + print(perl_store_paths) + assert len(perl_store_paths) == 0 + ''; + +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 9e27969190f75..1453a3875f6e7 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -285,6 +285,9 @@ in { activation = pkgs.callPackage ../modules/system/activation/test.nix { }; activation-var = runTest ./activation/var.nix; activation-nix-channel = runTest ./activation/nix-channel.nix; + activation-etc-overlay-mutable = runTest ./activation/etc-overlay-mutable.nix; + activation-etc-overlay-immutable = runTest ./activation/etc-overlay-immutable.nix; + activation-perlless = runTest ./activation/perlless.nix; etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {}; etcd-cluster = handleTestOn ["x86_64-linux"] ./etcd-cluster.nix {}; etebase-server = handleTest ./etebase-server.nix {}; @@ -569,8 +572,8 @@ in { netdata = handleTest ./netdata.nix {}; networking.networkd = handleTest ./networking.nix { networkd = true; }; networking.scripted = handleTest ./networking.nix { networkd = false; }; - netbox_3_5 = handleTest ./web-apps/netbox.nix { netbox = pkgs.netbox_3_5; }; netbox_3_6 = handleTest ./web-apps/netbox.nix { netbox = pkgs.netbox_3_6; }; + netbox_3_7 = handleTest ./web-apps/netbox.nix { netbox = pkgs.netbox_3_7; }; netbox-upgrade = handleTest ./web-apps/netbox-upgrade.nix {}; # TODO: put in networking.nix after the test becomes more complete networkingProxy = handleTest ./networking-proxy.nix {}; @@ -866,6 +869,8 @@ in { systemd-repart = handleTest ./systemd-repart.nix {}; systemd-shutdown = handleTest ./systemd-shutdown.nix {}; systemd-sysupdate = runTest ./systemd-sysupdate.nix; + systemd-sysusers-mutable = runTest ./systemd-sysusers-mutable.nix; + systemd-sysusers-immutable = runTest ./systemd-sysusers-immutable.nix; systemd-timesyncd = handleTest ./systemd-timesyncd.nix {}; systemd-timesyncd-nscd-dnssec = handleTest ./systemd-timesyncd-nscd-dnssec.nix {}; systemd-user-tmpfiles-rules = handleTest ./systemd-user-tmpfiles-rules.nix {}; @@ -905,6 +910,7 @@ in { trilium-server = handleTestOn ["x86_64-linux"] ./trilium-server.nix {}; tsja = handleTest ./tsja.nix {}; tsm-client-gui = handleTest ./tsm-client-gui.nix {}; + ttyd = handleTest ./web-servers/ttyd.nix {}; txredisapi = handleTest ./txredisapi.nix {}; tuptime = handleTest ./tuptime.nix {}; turbovnc-headless-server = handleTest ./turbovnc-headless-server.nix {}; diff --git a/nixos/tests/appliance-repart-image.nix b/nixos/tests/appliance-repart-image.nix index 1c4495baba131..b18968d3b9631 100644 --- a/nixos/tests/appliance-repart-image.nix +++ b/nixos/tests/appliance-repart-image.nix @@ -10,10 +10,6 @@ let imageId = "nixos-appliance"; imageVersion = "1-rc1"; - - bootLoaderConfigPath = "/loader/entries/nixos.conf"; - kernelPath = "/EFI/nixos/kernel.efi"; - initrdPath = "/EFI/nixos/initrd.efi"; in { name = "appliance-gpt-image"; @@ -54,19 +50,8 @@ in "/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI".source = "${pkgs.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; - # TODO: create an abstraction for Boot Loader Specification (BLS) entries. - "${bootLoaderConfigPath}".source = pkgs.writeText "nixos.conf" '' - title NixOS - linux ${kernelPath} - initrd ${initrdPath} - options init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} - ''; - - "${kernelPath}".source = - "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}"; - - "${initrdPath}".source = - "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}"; + "/EFI/Linux/${config.system.boot.loader.ukiFile}".source = + "${config.system.build.uki}/${config.system.boot.loader.ukiFile}"; }; repartConfig = { Type = "esp"; @@ -119,8 +104,6 @@ in assert 'IMAGE_VERSION="${imageVersion}"' in os_release bootctl_status = machine.succeed("bootctl status") - assert "${bootLoaderConfigPath}" in bootctl_status - assert "${kernelPath}" in bootctl_status - assert "${initrdPath}" in bootctl_status + assert "Boot Loader Specification Type #2 (.efi)" in bootctl_status ''; } diff --git a/nixos/tests/elk.nix b/nixos/tests/elk.nix index 900ea6320100f..b5a8cb532ae0a 100644 --- a/nixos/tests/elk.nix +++ b/nixos/tests/elk.nix @@ -1,6 +1,6 @@ # To run the test on the unfree ELK use the following command: # cd path/to/nixpkgs -# NIXPKGS_ALLOW_UNFREE=1 nix-build -A nixosTests.elk.unfree.ELK-6 +# NIXPKGS_ALLOW_UNFREE=1 nix-build -A nixosTests.elk.unfree.ELK-7 { system ? builtins.currentSystem, config ? {}, @@ -120,7 +120,7 @@ let }; elasticsearch-curator = { - enable = true; + enable = elk ? elasticsearch-curator; actionYAML = '' --- actions: @@ -246,7 +246,7 @@ let one.wait_until_succeeds( expect_hits("SuperdupercalifragilisticexpialidociousIndeed") ) - '' + '' + '' + lib.optionalString (elk ? elasticsearch-curator) '' with subtest("Elasticsearch-curator works"): one.systemctl("stop logstash") one.systemctl("start elasticsearch-curator") diff --git a/nixos/tests/keepalived.nix b/nixos/tests/keepalived.nix index d0bf9d4652003..ce291514591fe 100644 --- a/nixos/tests/keepalived.nix +++ b/nixos/tests/keepalived.nix @@ -1,5 +1,6 @@ -import ./make-test-python.nix ({ pkgs, ... }: { +import ./make-test-python.nix ({ pkgs, lib, ... }: { name = "keepalived"; + maintainers = [ lib.maintainers.raitobezarius ]; nodes = { node1 = { pkgs, ... }: { diff --git a/nixos/tests/systemd-sysusers-immutable.nix b/nixos/tests/systemd-sysusers-immutable.nix new file mode 100644 index 0000000000000..42cbf84d175e4 --- /dev/null +++ b/nixos/tests/systemd-sysusers-immutable.nix @@ -0,0 +1,64 @@ +{ lib, ... }: + +let + rootPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6"; + normaloPassword = "$y$j9T$3aiOV/8CADAK22OK2QT3/0$67OKd50Z4qTaZ8c/eRWHLIM.o3ujtC1.n9ysmJfv639"; + newNormaloPassword = "mellow"; +in + +{ + + name = "activation-sysusers-immutable"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { + systemd.sysusers.enable = true; + users.mutableUsers = false; + + # Override the empty root password set by the test instrumentation + users.users.root.hashedPasswordFile = lib.mkForce null; + users.users.root.initialHashedPassword = rootPassword; + users.users.normalo = { + isNormalUser = true; + initialHashedPassword = normaloPassword; + }; + + specialisation.new-generation.configuration = { + users.users.new-normalo = { + isNormalUser = true; + initialPassword = newNormaloPassword; + }; + }; + }; + + testScript = '' + with subtest("Users are not created with systemd-sysusers"): + machine.fail("systemctl status systemd-sysusers.service") + machine.fail("ls /etc/sysusers.d") + + with subtest("Correct mode on the password files"): + assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n" + assert machine.succeed("stat -c '%a' /etc/group") == "644\n" + assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n" + assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n" + + with subtest("root user has correct password"): + print(machine.succeed("getent passwd root")) + assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct" + + with subtest("normalo user is created"): + print(machine.succeed("getent passwd normalo")) + assert machine.succeed("stat -c '%U' /home/normalo") == "normalo\n" + assert "${normaloPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not correct" + + + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch") + + + with subtest("new-normalo user is created after switching to new generation"): + print(machine.succeed("getent passwd new-normalo")) + print(machine.succeed("getent shadow new-normalo")) + assert machine.succeed("stat -c '%U' /home/new-normalo") == "new-normalo\n" + ''; +} diff --git a/nixos/tests/systemd-sysusers-mutable.nix b/nixos/tests/systemd-sysusers-mutable.nix new file mode 100644 index 0000000000000..e69cfe23a59a1 --- /dev/null +++ b/nixos/tests/systemd-sysusers-mutable.nix @@ -0,0 +1,71 @@ +{ lib, ... }: + +let + rootPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6"; + normaloPassword = "hello"; + newNormaloPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6"; +in + +{ + + name = "activation-sysusers-mutable"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { pkgs, ... }: { + systemd.sysusers.enable = true; + users.mutableUsers = true; + + # Prerequisites + system.etc.overlay.enable = true; + boot.initrd.systemd.enable = true; + boot.kernelPackages = pkgs.linuxPackages_latest; + + # Override the empty root password set by the test instrumentation + users.users.root.hashedPasswordFile = lib.mkForce null; + users.users.root.initialHashedPassword = rootPassword; + users.users.normalo = { + isNormalUser = true; + initialPassword = normaloPassword; + }; + + specialisation.new-generation.configuration = { + users.users.new-normalo = { + isNormalUser = true; + initialHashedPassword = newNormaloPassword; + }; + }; + }; + + testScript = '' + machine.wait_for_unit("systemd-sysusers.service") + + with subtest("systemd-sysusers.service contains the credentials"): + sysusers_service = machine.succeed("systemctl cat systemd-sysusers.service") + print(sysusers_service) + assert "SetCredential=passwd.plaintext-password.normalo:${normaloPassword}" in sysusers_service + + with subtest("Correct mode on the password files"): + assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n" + assert machine.succeed("stat -c '%a' /etc/group") == "644\n" + assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n" + assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n" + + with subtest("root user has correct password"): + print(machine.succeed("getent passwd root")) + assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct" + + with subtest("normalo user is created"): + print(machine.succeed("getent passwd normalo")) + assert machine.succeed("stat -c '%U' /home/normalo") == "normalo\n" + + + machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch") + + + with subtest("new-normalo user is created after switching to new generation"): + print(machine.succeed("getent passwd new-normalo")) + assert machine.succeed("stat -c '%U' /home/new-normalo") == "new-normalo\n" + assert "${newNormaloPassword}" in machine.succeed("getent shadow new-normalo"), "new-normalo user password is not correct" + ''; +} diff --git a/nixos/tests/web-apps/netbox-upgrade.nix b/nixos/tests/web-apps/netbox-upgrade.nix index b5403eb678bcb..4c554e7ae613b 100644 --- a/nixos/tests/web-apps/netbox-upgrade.nix +++ b/nixos/tests/web-apps/netbox-upgrade.nix @@ -1,6 +1,6 @@ import ../make-test-python.nix ({ lib, pkgs, ... }: let - oldNetbox = pkgs.netbox_3_5; - newNetbox = pkgs.netbox_3_6; + oldNetbox = pkgs.netbox_3_6; + newNetbox = pkgs.netbox_3_7; in { name = "netbox-upgrade"; diff --git a/nixos/tests/web-servers/stargazer.nix b/nixos/tests/web-servers/stargazer.nix index 6365d6a4fff10..f56d1b8c94545 100644 --- a/nixos/tests/web-servers/stargazer.nix +++ b/nixos/tests/web-servers/stargazer.nix @@ -1,4 +1,41 @@ { pkgs, lib, ... }: +let + test_script = pkgs.stdenv.mkDerivation rec { + pname = "stargazer-test-script"; + inherit (pkgs.stargazer) version src; + buildInputs = with pkgs; [ (python3.withPackages (ps: with ps; [ cryptography ])) ]; + dontBuild = true; + doCheck = false; + installPhase = '' + mkdir -p $out/bin + cp scripts/gemini-diagnostics $out/bin/test + ''; + }; + test_env = pkgs.stdenv.mkDerivation rec { + pname = "stargazer-test-env"; + inherit (pkgs.stargazer) version src; + buildPhase = '' + cc test_data/cgi-bin/loop.c -o test_data/cgi-bin/loop + ''; + doCheck = false; + installPhase = '' + mkdir -p $out + cp -r * $out/ + ''; + }; + scgi_server = pkgs.stdenv.mkDerivation rec { + pname = "stargazer-test-scgi-server"; + inherit (pkgs.stargazer) version src; + buildInputs = with pkgs; [ python3 ]; + dontConfigure = true; + dontBuild = true; + doCheck = false; + installPhase = '' + mkdir -p $out/bin + cp scripts/scgi-server $out/bin/scgi-server + ''; + }; +in { name = "stargazer"; meta = with lib.maintainers; { maintainers = [ gaykitty ]; }; @@ -7,25 +44,84 @@ geminiserver = { pkgs, ... }: { services.stargazer = { enable = true; + connectionLogging = false; + requestTimeout = 1; routes = [ { route = "localhost"; - root = toString (pkgs.writeTextDir "index.gmi" '' - # Hello NixOS! - ''); + root = "${test_env}/test_data/test_site"; + } + { + route = "localhost=/en.gmi"; + root = "${test_env}/test_data/test_site"; + lang = "en"; + charset = "ascii"; + } + { + route = "localhost~/(.*).gemini"; + root = "${test_env}/test_data/test_site"; + rewrite = "\\1.gmi"; + lang = "en"; + charset = "ascii"; + } + { + route = "localhost=/plain.txt"; + root = "${test_env}/test_data/test_site"; + lang = "en"; + charset = "ascii"; + cert-path = "/var/lib/gemini/certs/localhost.crt"; + key-path = "/var/lib/gemini/certs/localhost.key"; + } + { + route = "localhost:/cgi-bin"; + root = "${test_env}/test_data"; + cgi = true; + cgi-timeout = 5; + } + { + route = "localhost:/scgi"; + scgi = true; + scgi-address = "127.0.0.1:1099"; + } + { + route = "localhost=/root"; + redirect = ".."; + permanent = true; + } + { + route = "localhost=/priv.gmi"; + root = "${test_env}/test_data/test_site"; + client-cert = "${test_env}/test_data/client_cert/good.crt"; + } + { + route = "example.com~(.*)"; + redirect = "gemini://localhost"; + rewrite = "\\1"; + } + { + route = "localhost:/no-exist"; + root = "./does_not_exist"; } ]; }; + systemd.services.scgi_server = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${scgi_server}/bin/scgi-server"; + }; + }; }; }; testScript = { nodes, ... }: '' + geminiserver.wait_for_unit("scgi_server") + geminiserver.wait_for_open_port(1099) geminiserver.wait_for_unit("stargazer") geminiserver.wait_for_open_port(1965) - with subtest("check is serving over gemini"): - response = geminiserver.succeed("${pkgs.gemget}/bin/gemget --header -o - gemini://localhost:1965") + with subtest("stargazer test suite"): + response = geminiserver.succeed("sh -c 'cd ${test_env}; ${test_script}/bin/test'") print(response) - assert "Hello NixOS!" in response ''; } diff --git a/nixos/tests/web-servers/ttyd.nix b/nixos/tests/web-servers/ttyd.nix new file mode 100644 index 0000000000000..d161673684b31 --- /dev/null +++ b/nixos/tests/web-servers/ttyd.nix @@ -0,0 +1,19 @@ +import ../make-test-python.nix ({ lib, pkgs, ... }: { + name = "ttyd"; + meta.maintainers = with lib.maintainers; [ stunkymonkey ]; + + nodes.machine = { pkgs, ... }: { + services.ttyd = { + enable = true; + username = "foo"; + passwordFile = pkgs.writeText "password" "bar"; + }; + }; + + testScript = '' + machine.wait_for_unit("ttyd.service") + machine.wait_for_open_port(7681) + response = machine.succeed("curl -vvv -u foo:bar -s -H 'Host: ttyd' http://127.0.0.1:7681/") + assert '<title>ttyd - Terminal</title>' in response, "Page didn't load successfully" + ''; +}) |