Add modular services, system.services

Changed files
+433
nixos
+2
nixos/modules/module-list.nix
···
./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
+28
nixos/modules/system/service/README.md
···
+
+
# 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
+
+58
nixos/modules/system/service/portable/service.nix
···
+
{
+
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 = [ ];
+
};
+
};
+
};
+
}
+93
nixos/modules/system/service/portable/test.nix
···
+
# 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
+79
nixos/modules/system/service/systemd/service.nix
···
+
{
+
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
+
))
+
];
+
};
+
};
+
};
+
}
+68
nixos/modules/system/service/systemd/system.nix
···
+
{
+
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;
+
};
+
}
+89
nixos/modules/system/service/systemd/test.nix
···
+
# 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
+
''
+3
nixos/modules/system/service/systemd/user.nix
···
+
# TBD, analogous to system.nix but for user units
+
{
+
}
+13
nixos/tests/all-tests.nix
···
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 {
···
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;