Add modular services, system.services

This commit is contained in:
Robert Hensing
2025-07-20 01:35:12 +02:00
parent b915f0c5c0
commit 1acabeebed
9 changed files with 433 additions and 0 deletions

View File

@@ -1838,6 +1838,8 @@
./system/boot/uvesafb.nix
./system/boot/zram-as-tmp.nix
./system/etc/etc-activation.nix
./system/service/systemd/system.nix
./system/service/systemd/user.nix
./tasks/auto-upgrade.nix
./tasks/bcache.nix
./tasks/cpu-freq.nix

View File

@@ -0,0 +1,28 @@
# Modular Services
This directory defines a modular service infrastructure for NixOS.
See the [Modular Services chapter] in the manual [[source]](../../doc/manual/development/modular-services.md).
[Modular Services chapter]: https://nixos.org/manual/nixos/unstable/#modular-services
# Design decision log
- `system.services.<name>`. Alternatives considered
- `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.
- `services.abstract`: used in https://github.com/NixOS/nixpkgs/pull/267111, but too weird. Service modules should fit naturally into the configuration system.
Also "abstract" is wrong, because it has submodules - in other words, evalModules results, concrete services - not abstract at all.
- `services.modular`: only slightly better than `services.abstract`, but still weird
- No `daemon.*` options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521
- 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)?
- 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.
- `modules/system/service/systemd/system.nix` has `system` twice. Not great, but
- they have different meanings
1. These are system-provided modules, provided by the configuration manager
2. `systemd/system` configures SystemD _system units_.
- This reserves `modules/service` for actual service modules, at least until those are lifted out of NixOS, potentially

View File

@@ -0,0 +1,58 @@
{
lib,
config,
options,
...
}:
let
inherit (lib) mkOption types;
pathOrStr = types.coercedTo types.path (x: "${x}") types.str;
program =
types.coercedTo (
types.package
// {
# require mainProgram for this conversion
check = v: v.type or null == "derivation" && v ? meta.mainProgram;
}
) lib.getExe pathOrStr
// {
description = "main program, path or command";
descriptionClass = "conjunction";
};
in
{
options = {
services = mkOption {
type = types.attrsOf (
types.submoduleWith {
modules = [
./service.nix
];
}
);
description = ''
A collection of [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured in one go.
You could consider the sub-service relationship to be an ownership relation.
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.
'';
default = { };
visible = "shallow";
};
process = {
executable = mkOption {
type = program;
description = ''
The path to the executable that will be run when the service is started.
'';
};
args = lib.mkOption {
type = types.listOf pathOrStr;
description = ''
Arguments to pass to the `executable`.
'';
default = [ ];
};
};
};
}

View File

@@ -0,0 +1,93 @@
# Run:
# nix-instantiate --eval nixos/modules/system/service/portable/test.nix
let
lib = import ../../../../../lib;
inherit (lib) mkOption types;
dummyPkg =
name:
derivation {
system = "dummy";
name = name;
builder = "/bin/false";
};
exampleConfig = {
_file = "${__curPos.file}:${toString __curPos.line}";
services = {
service1 = {
process = {
executable = "/usr/bin/echo"; # *giggles*
args = [ "hello" ];
};
};
service2 = {
process = {
# No meta.mainProgram, because it's supposedly an executable script _file_,
# not a directory with a bin directory containing the main program.
executable = dummyPkg "cowsay.sh";
args = [ "world" ];
};
};
service3 = {
process = {
executable = dummyPkg "cowsay-ng" // {
meta.mainProgram = "cowsay";
};
args = [ "!" ];
};
};
};
};
exampleEval = lib.evalModules {
modules = [
{
options.services = mkOption {
type = types.attrsOf (
types.submoduleWith {
class = "service";
modules = [
./service.nix
];
}
);
};
}
exampleConfig
];
};
test =
assert
exampleEval.config == {
services = {
service1 = {
process = {
executable = "/usr/bin/echo";
args = [ "hello" ];
};
services = { };
};
service2 = {
process = {
executable = "${dummyPkg "cowsay.sh"}";
args = [ "world" ];
};
services = { };
};
service3 = {
process = {
executable = "${dummyPkg "cowsay-ng"}/bin/cowsay";
args = [ "!" ];
};
services = { };
};
};
};
"ok";
in
test

View File

@@ -0,0 +1,79 @@
{
lib,
config,
systemdPackage,
...
}:
let
inherit (lib) mkOption types;
in
{
imports = [
../portable/service.nix
(lib.mkAliasOptionModule [ "systemd" "service" ] [ "systemd" "services" "" ])
(lib.mkAliasOptionModule [ "systemd" "socket" ] [ "systemd" "sockets" "" ])
];
options = {
systemd.services = mkOption {
description = ''
This module configures systemd services, with the notable difference that their unit names will be prefixed with the abstract service name.
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.
Note that this option contains _deferred_ modules.
This means that the module has not been combined with the system configuration yet, no values can be read from this option.
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.
'';
type = types.lazyAttrsOf (
types.deferredModuleWith {
staticModules = [
# TODO: Add modules for the purpose of generating documentation?
];
}
);
default = { };
};
systemd.sockets = mkOption {
description = ''
Declares systemd socket units. Names will be prefixed by the service name / path.
See {option}`systemd.services`.
'';
type = types.lazyAttrsOf types.deferredModule;
default = { };
};
# Also import systemd logic into sub-services
# extends the portable `services` option
services = mkOption {
type = types.attrsOf (
types.submoduleWith {
class = "service";
modules = [
./service.nix
];
specialArgs = {
inherit systemdPackage;
};
}
);
};
};
config = {
# Note that this is the systemd.services option above, not the system one.
systemd.services."" = {
# TODO description;
wantedBy = lib.mkDefault [ "multi-user.target" ];
serviceConfig = {
Type = lib.mkDefault "simple";
Restart = lib.mkDefault "always";
RestartSec = lib.mkDefault "5";
ExecStart = [
(systemdPackage.functions.escapeSystemdExecArgs (
[ config.process.executable ] ++ config.process.args
))
];
};
};
};
}

View File

@@ -0,0 +1,68 @@
{
lib,
config,
pkgs,
...
}:
let
inherit (lib) concatMapAttrs mkOption types;
dash =
before: after:
if after == "" then
before
else if before == "" then
after
else
"${before}-${after}";
makeUnits =
unitType: prefix: service:
concatMapAttrs (unitName: unitModule: {
"${dash prefix unitName}" =
{ ... }:
{
imports = [ unitModule ];
};
}) service.systemd.${unitType}
// concatMapAttrs (
subServiceName: subService: makeUnits unitType (dash prefix subServiceName) subService
) service.services;
in
{
# First half of the magic: mix systemd logic into the otherwise abstract services
options = {
system.services = mkOption {
description = ''
A collection of NixOS [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured as systemd services.
'';
type = types.attrsOf (
types.submoduleWith {
class = "service";
modules = [
./service.nix
];
specialArgs = {
# perhaps: features."systemd" = { };
inherit pkgs;
systemdPackage = config.systemd.package;
};
}
);
default = { };
visible = "shallow";
};
};
# Second half of the magic: siphon units that were defined in isolation to the system
config = {
systemd.services = concatMapAttrs (
serviceName: topLevelService: makeUnits "services" serviceName topLevelService
) config.system.services;
systemd.sockets = concatMapAttrs (
serviceName: topLevelService: makeUnits "sockets" serviceName topLevelService
) config.system.services;
};
}

View File

@@ -0,0 +1,89 @@
# Run:
# nix-build -A nixosTests.modularService
{
evalSystem,
runCommand,
hello,
...
}:
let
machine = evalSystem (
{ lib, ... }:
{
# Test input
system.services.foo = {
process = {
executable = hello;
args = [
"--greeting"
"hoi"
];
};
};
system.services.bar = {
process = {
executable = hello;
args = [
"--greeting"
"hoi"
];
};
systemd.service = {
serviceConfig.X-Bar = "lol crossbar whatever";
};
services.db = {
process = {
executable = hello;
args = [
"--greeting"
"Hi, I'm a database, would you believe it"
];
};
systemd.service = {
serviceConfig.RestartSec = "42";
};
};
};
# irrelevant stuff
system.stateVersion = "25.05";
fileSystems."/".device = "/test/dummy";
boot.loader.grub.enable = false;
}
);
inherit (machine.config.system.build) toplevel;
in
runCommand "test-modular-service-systemd-units"
{
passthru = {
inherit
machine
toplevel
;
};
}
''
echo ${toplevel}/etc/systemd/system/foo.service:
cat -n ${toplevel}/etc/systemd/system/foo.service
(
set -x
grep -F 'ExecStart=${hello}/bin/hello --greeting hoi' ${toplevel}/etc/systemd/system/foo.service >/dev/null
grep -F 'ExecStart=${hello}/bin/hello --greeting hoi' ${toplevel}/etc/systemd/system/bar.service >/dev/null
grep -F 'X-Bar=lol crossbar whatever' ${toplevel}/etc/systemd/system/bar.service >/dev/null
grep 'ExecStart=${hello}/bin/hello --greeting .*database.*' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null
grep -F 'RestartSec=42' ${toplevel}/etc/systemd/system/bar-db.service >/dev/null
[[ ! -e ${toplevel}/etc/systemd/system/foo.socket ]]
[[ ! -e ${toplevel}/etc/systemd/system/bar.socket ]]
[[ ! -e ${toplevel}/etc/systemd/system/bar-db.socket ]]
)
echo 🐬👍
touch $out
''

View File

@@ -0,0 +1,3 @@
# TBD, analogous to system.nix but for user units
{
}

View File

@@ -89,6 +89,16 @@ let
featureFlags.minimalModules = { };
};
evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; };
evalSystem =
module:
import ../lib/eval-config.nix {
system = null;
modules = [
../modules/misc/nixpkgs/read-only.nix
{ nixpkgs.pkgs = pkgs; }
module
];
};
inherit
(rec {
@@ -891,6 +901,9 @@ in
mjolnir = runTest ./matrix/mjolnir.nix;
mobilizon = runTest ./mobilizon.nix;
mod_perl = runTest ./mod_perl.nix;
modularService = pkgs.callPackage ../modules/system/service/systemd/test.nix {
inherit evalSystem;
};
molly-brown = runTest ./molly-brown.nix;
mollysocket = runTest ./mollysocket.nix;
monado = runTest ./monado.nix;