Add modular services, system.services

+433
+2
nixos/modules/module-list.nix
··· 1838 1838 ./system/boot/uvesafb.nix 1839 1839 ./system/boot/zram-as-tmp.nix 1840 1840 ./system/etc/etc-activation.nix 1841 + ./system/service/systemd/system.nix 1842 + ./system/service/systemd/user.nix 1841 1843 ./tasks/auto-upgrade.nix 1842 1844 ./tasks/bcache.nix 1843 1845 ./tasks/cpu-freq.nix
+28
nixos/modules/system/service/README.md
··· 1 + 2 + # Modular Services 3 + 4 + This directory defines a modular service infrastructure for NixOS. 5 + See the [Modular Services chapter] in the manual [[source]](../../doc/manual/development/modular-services.md). 6 + 7 + [Modular Services chapter]: https://nixos.org/manual/nixos/unstable/#modular-services 8 + 9 + # Design decision log 10 + 11 + - `system.services.<name>`. Alternatives considered 12 + - `systemServices`: similar to does not allow importing a composition of services into `system`. Not sure if that's a good idea in the first place, but I've kept the possibility open. 13 + - `services.abstract`: used in https://github.com/NixOS/nixpkgs/pull/267111, but too weird. Service modules should fit naturally into the configuration system. 14 + Also "abstract" is wrong, because it has submodules - in other words, evalModules results, concrete services - not abstract at all. 15 + - `services.modular`: only slightly better than `services.abstract`, but still weird 16 + 17 + - No `daemon.*` options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521 18 + 19 + - For now, do not add an `enable` option, because it's ambiguous. Does it disable at the Nix level (not generate anything) or at the systemd level (generate a service that is disabled)? 20 + 21 + - Move all process options into a `process` option tree. Putting this at the root is messy, because we also have sub-services at that level. Those are rather distinct. Grouping them "by kind" should raise fewer questions. 22 + 23 + - `modules/system/service/systemd/system.nix` has `system` twice. Not great, but 24 + - they have different meanings 25 + 1. These are system-provided modules, provided by the configuration manager 26 + 2. `systemd/system` configures SystemD _system units_. 27 + - This reserves `modules/service` for actual service modules, at least until those are lifted out of NixOS, potentially 28 +
+58
nixos/modules/system/service/portable/service.nix
··· 1 + { 2 + lib, 3 + config, 4 + options, 5 + ... 6 + }: 7 + let 8 + inherit (lib) mkOption types; 9 + pathOrStr = types.coercedTo types.path (x: "${x}") types.str; 10 + program = 11 + types.coercedTo ( 12 + types.package 13 + // { 14 + # require mainProgram for this conversion 15 + check = v: v.type or null == "derivation" && v ? meta.mainProgram; 16 + } 17 + ) lib.getExe pathOrStr 18 + // { 19 + description = "main program, path or command"; 20 + descriptionClass = "conjunction"; 21 + }; 22 + in 23 + { 24 + options = { 25 + services = mkOption { 26 + type = types.attrsOf ( 27 + types.submoduleWith { 28 + modules = [ 29 + ./service.nix 30 + ]; 31 + } 32 + ); 33 + description = '' 34 + A collection of [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured in one go. 35 + 36 + You could consider the sub-service relationship to be an ownership relation. 37 + It **does not** automatically create any other relationship between services (e.g. systemd slices), unless perhaps such a behavior is explicitly defined and enabled in another option. 38 + ''; 39 + default = { }; 40 + visible = "shallow"; 41 + }; 42 + process = { 43 + executable = mkOption { 44 + type = program; 45 + description = '' 46 + The path to the executable that will be run when the service is started. 47 + ''; 48 + }; 49 + args = lib.mkOption { 50 + type = types.listOf pathOrStr; 51 + description = '' 52 + Arguments to pass to the `executable`. 53 + ''; 54 + default = [ ]; 55 + }; 56 + }; 57 + }; 58 + }
+93
nixos/modules/system/service/portable/test.nix
··· 1 + # Run: 2 + # nix-instantiate --eval nixos/modules/system/service/portable/test.nix 3 + let 4 + lib = import ../../../../../lib; 5 + 6 + inherit (lib) mkOption types; 7 + 8 + dummyPkg = 9 + name: 10 + derivation { 11 + system = "dummy"; 12 + name = name; 13 + builder = "/bin/false"; 14 + }; 15 + 16 + exampleConfig = { 17 + _file = "${__curPos.file}:${toString __curPos.line}"; 18 + services = { 19 + service1 = { 20 + process = { 21 + executable = "/usr/bin/echo"; # *giggles* 22 + args = [ "hello" ]; 23 + }; 24 + }; 25 + service2 = { 26 + process = { 27 + # No meta.mainProgram, because it's supposedly an executable script _file_, 28 + # not a directory with a bin directory containing the main program. 29 + executable = dummyPkg "cowsay.sh"; 30 + args = [ "world" ]; 31 + }; 32 + }; 33 + service3 = { 34 + process = { 35 + executable = dummyPkg "cowsay-ng" // { 36 + meta.mainProgram = "cowsay"; 37 + }; 38 + args = [ "!" ]; 39 + }; 40 + }; 41 + }; 42 + }; 43 + 44 + exampleEval = lib.evalModules { 45 + modules = [ 46 + { 47 + options.services = mkOption { 48 + type = types.attrsOf ( 49 + types.submoduleWith { 50 + class = "service"; 51 + modules = [ 52 + ./service.nix 53 + ]; 54 + } 55 + ); 56 + }; 57 + } 58 + exampleConfig 59 + ]; 60 + }; 61 + 62 + test = 63 + assert 64 + exampleEval.config == { 65 + services = { 66 + service1 = { 67 + process = { 68 + executable = "/usr/bin/echo"; 69 + args = [ "hello" ]; 70 + }; 71 + services = { }; 72 + }; 73 + service2 = { 74 + process = { 75 + executable = "${dummyPkg "cowsay.sh"}"; 76 + args = [ "world" ]; 77 + }; 78 + services = { }; 79 + }; 80 + service3 = { 81 + process = { 82 + executable = "${dummyPkg "cowsay-ng"}/bin/cowsay"; 83 + args = [ "!" ]; 84 + }; 85 + services = { }; 86 + }; 87 + }; 88 + }; 89 + 90 + "ok"; 91 + 92 + in 93 + test
+79
nixos/modules/system/service/systemd/service.nix
··· 1 + { 2 + lib, 3 + config, 4 + systemdPackage, 5 + ... 6 + }: 7 + let 8 + inherit (lib) mkOption types; 9 + in 10 + { 11 + imports = [ 12 + ../portable/service.nix 13 + (lib.mkAliasOptionModule [ "systemd" "service" ] [ "systemd" "services" "" ]) 14 + (lib.mkAliasOptionModule [ "systemd" "socket" ] [ "systemd" "sockets" "" ]) 15 + ]; 16 + options = { 17 + systemd.services = mkOption { 18 + description = '' 19 + This module configures systemd services, with the notable difference that their unit names will be prefixed with the abstract service name. 20 + 21 + This option's value is not suitable for reading, but you can define a module here that interacts with just the unit configuration in the host system configuration. 22 + 23 + Note that this option contains _deferred_ modules. 24 + This means that the module has not been combined with the system configuration yet, no values can be read from this option. 25 + What you can do instead is define a module that reads from the module arguments (such as `config`) that are available when the module is merged into the system configuration. 26 + ''; 27 + type = types.lazyAttrsOf ( 28 + types.deferredModuleWith { 29 + staticModules = [ 30 + # TODO: Add modules for the purpose of generating documentation? 31 + ]; 32 + } 33 + ); 34 + default = { }; 35 + }; 36 + systemd.sockets = mkOption { 37 + description = '' 38 + Declares systemd socket units. Names will be prefixed by the service name / path. 39 + 40 + See {option}`systemd.services`. 41 + ''; 42 + type = types.lazyAttrsOf types.deferredModule; 43 + default = { }; 44 + }; 45 + 46 + # Also import systemd logic into sub-services 47 + # extends the portable `services` option 48 + services = mkOption { 49 + type = types.attrsOf ( 50 + types.submoduleWith { 51 + class = "service"; 52 + modules = [ 53 + ./service.nix 54 + ]; 55 + specialArgs = { 56 + inherit systemdPackage; 57 + }; 58 + } 59 + ); 60 + }; 61 + }; 62 + config = { 63 + # Note that this is the systemd.services option above, not the system one. 64 + systemd.services."" = { 65 + # TODO description; 66 + wantedBy = lib.mkDefault [ "multi-user.target" ]; 67 + serviceConfig = { 68 + Type = lib.mkDefault "simple"; 69 + Restart = lib.mkDefault "always"; 70 + RestartSec = lib.mkDefault "5"; 71 + ExecStart = [ 72 + (systemdPackage.functions.escapeSystemdExecArgs ( 73 + [ config.process.executable ] ++ config.process.args 74 + )) 75 + ]; 76 + }; 77 + }; 78 + }; 79 + }
+68
nixos/modules/system/service/systemd/system.nix
··· 1 + { 2 + lib, 3 + config, 4 + pkgs, 5 + ... 6 + }: 7 + 8 + let 9 + inherit (lib) concatMapAttrs mkOption types; 10 + 11 + dash = 12 + before: after: 13 + if after == "" then 14 + before 15 + else if before == "" then 16 + after 17 + else 18 + "${before}-${after}"; 19 + 20 + makeUnits = 21 + unitType: prefix: service: 22 + concatMapAttrs (unitName: unitModule: { 23 + "${dash prefix unitName}" = 24 + { ... }: 25 + { 26 + imports = [ unitModule ]; 27 + }; 28 + }) service.systemd.${unitType} 29 + // concatMapAttrs ( 30 + subServiceName: subService: makeUnits unitType (dash prefix subServiceName) subService 31 + ) service.services; 32 + in 33 + { 34 + # First half of the magic: mix systemd logic into the otherwise abstract services 35 + options = { 36 + system.services = mkOption { 37 + description = '' 38 + A collection of NixOS [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured as systemd services. 39 + ''; 40 + type = types.attrsOf ( 41 + types.submoduleWith { 42 + class = "service"; 43 + modules = [ 44 + ./service.nix 45 + ]; 46 + specialArgs = { 47 + # perhaps: features."systemd" = { }; 48 + inherit pkgs; 49 + systemdPackage = config.systemd.package; 50 + }; 51 + } 52 + ); 53 + default = { }; 54 + visible = "shallow"; 55 + }; 56 + }; 57 + 58 + # Second half of the magic: siphon units that were defined in isolation to the system 59 + config = { 60 + systemd.services = concatMapAttrs ( 61 + serviceName: topLevelService: makeUnits "services" serviceName topLevelService 62 + ) config.system.services; 63 + 64 + systemd.sockets = concatMapAttrs ( 65 + serviceName: topLevelService: makeUnits "sockets" serviceName topLevelService 66 + ) config.system.services; 67 + }; 68 + }
+89
nixos/modules/system/service/systemd/test.nix
··· 1 + # Run: 2 + # nix-build -A nixosTests.modularService 3 + 4 + { 5 + evalSystem, 6 + runCommand, 7 + hello, 8 + ... 9 + }: 10 + 11 + let 12 + machine = evalSystem ( 13 + { lib, ... }: 14 + { 15 + 16 + # Test input 17 + 18 + system.services.foo = { 19 + process = { 20 + executable = hello; 21 + args = [ 22 + "--greeting" 23 + "hoi" 24 + ]; 25 + }; 26 + }; 27 + system.services.bar = { 28 + process = { 29 + executable = hello; 30 + args = [ 31 + "--greeting" 32 + "hoi" 33 + ]; 34 + }; 35 + systemd.service = { 36 + serviceConfig.X-Bar = "lol crossbar whatever"; 37 + }; 38 + services.db = { 39 + process = { 40 + executable = hello; 41 + args = [ 42 + "--greeting" 43 + "Hi, I'm a database, would you believe it" 44 + ]; 45 + }; 46 + systemd.service = { 47 + serviceConfig.RestartSec = "42"; 48 + }; 49 + }; 50 + }; 51 + 52 + # irrelevant stuff 53 + system.stateVersion = "25.05"; 54 + fileSystems."/".device = "/test/dummy"; 55 + boot.loader.grub.enable = false; 56 + } 57 + ); 58 + 59 + inherit (machine.config.system.build) toplevel; 60 + in 61 + runCommand "test-modular-service-systemd-units" 62 + { 63 + passthru = { 64 + inherit 65 + machine 66 + toplevel 67 + ; 68 + }; 69 + } 70 + '' 71 + echo ${toplevel}/etc/systemd/system/foo.service: 72 + cat -n ${toplevel}/etc/systemd/system/foo.service 73 + ( 74 + set -x 75 + grep -F 'ExecStart=${hello}/bin/hello --greeting hoi' ${toplevel}/etc/systemd/system/foo.service >/dev/null 76 + 77 + grep -F 'ExecStart=${hello}/bin/hello --greeting hoi' ${toplevel}/etc/systemd/system/bar.service >/dev/null 78 + grep -F 'X-Bar=lol crossbar whatever' ${toplevel}/etc/systemd/system/bar.service >/dev/null 79 + 80 + grep 'ExecStart=${hello}/bin/hello --greeting .*database.*' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null 81 + grep -F 'RestartSec=42' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null 82 + 83 + [[ ! -e ${toplevel}/etc/systemd/system/foo.socket ]] 84 + [[ ! -e ${toplevel}/etc/systemd/system/bar.socket ]] 85 + [[ ! -e ${toplevel}/etc/systemd/system/bar-db.socket ]] 86 + ) 87 + echo 🐬👍 88 + touch $out 89 + ''
+3
nixos/modules/system/service/systemd/user.nix
··· 1 + # TBD, analogous to system.nix but for user units 2 + { 3 + }
+13
nixos/tests/all-tests.nix
··· 89 89 featureFlags.minimalModules = { }; 90 90 }; 91 91 evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; }; 92 + evalSystem = 93 + module: 94 + import ../lib/eval-config.nix { 95 + system = null; 96 + modules = [ 97 + ../modules/misc/nixpkgs/read-only.nix 98 + { nixpkgs.pkgs = pkgs; } 99 + module 100 + ]; 101 + }; 92 102 93 103 inherit 94 104 (rec { ··· 891 901 mjolnir = runTest ./matrix/mjolnir.nix; 892 902 mobilizon = runTest ./mobilizon.nix; 893 903 mod_perl = runTest ./mod_perl.nix; 904 + modularService = pkgs.callPackage ../modules/system/service/systemd/test.nix { 905 + inherit evalSystem; 906 + }; 894 907 molly-brown = runTest ./molly-brown.nix; 895 908 mollysocket = runTest ./mollysocket.nix; 896 909 monado = runTest ./monado.nix;