Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions nix/modules/systemd.nix
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for adding that feature ! Could you add an extra container test for that ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for sure. I've pushed a commit that adds an extra test.

That said, to merge this, we might also need to update the documentation. Previously, the docs stated that we define boot with type raw. I'm not sure exactly how it should be rephrased now, so I'll leave that to you if that's alright.

Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,21 @@ in

for package in $packages
do
for hook in $package/lib/systemd/system/*
for hook in $package/etc/systemd/system/* $package/lib/systemd/system/*
do
ln -s $hook $out/
done
done

for i in ${toString (lib.mapAttrsToList (n: v: v.unit) enabledUnits)}; do
for i in ${
toString (
lib.mapAttrsToList (n: v: v.unit) (
lib.filterAttrs (
n: v: (lib.attrByPath [ "overrideStrategy" ] "asDropinIfExists" v) == "asDropinIfExists"
) enabledUnits
)
)
}; do
fn=$(basename $i/*)
if [ -e $out/$fn ]; then
if [ "$(readlink -f $i/$fn)" = /dev/null ]; then
Expand All @@ -251,6 +259,18 @@ in
fi
done

for i in ${
toString (
lib.mapAttrsToList (n: v: v.unit) (
lib.filterAttrs (n: v: v ? overrideStrategy && v.overrideStrategy == "asDropin") enabledUnits
)
)
}; do
fn=$(basename $i/*)
mkdir -p $out/$fn.d
ln -s $i/$fn $out/$fn.d/overrides.conf
done

${lib.concatStrings (
lib.mapAttrsToList (
name: unit:
Expand Down
72 changes: 68 additions & 4 deletions nix/modules/upstream/nixpkgs/default.nix
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
{
nixosModulesPath,
lib,
pkgs,
config,
...
}:
let
modulesTypeDesc = ''
This can either be a list of modules, or an attrset. In an
attrset, names that are set to `true` represent modules that will
be included. Note that setting these names to `false` does not
prevent the module from being loaded.
'';
kernelModulesConf = pkgs.writeText "system-manager.conf" ''
${lib.concatStringsSep "\n" config.boot.kernelModules}
'';
attrNamesToTrue = lib.types.coercedTo (lib.types.listOf lib.types.str) (
enabledList: lib.genAttrs enabledList (_attrName: true)
) (lib.types.attrsOf lib.types.bool);
in
{
imports = [
./firewall.nix
Expand All @@ -22,6 +38,7 @@
"/security/acme/"
"/security/wrappers/"
"/services/web-servers/nginx/"
"/config/sysctl.nix"
# nix settings
"/config/nix.nix"
"/services/system/userborn.nix"
Expand All @@ -31,11 +48,27 @@
options =
# We need to ignore a bunch of options that are used in NixOS modules but
# that don't apply to system-manager configs.
# TODO: can we print an informational message for things like kernel modules
# to inform users that they need to be enabled in the host system?
{
boot = lib.mkOption {
type = lib.types.raw;
boot = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problems is that we now implement a subset of the boot options. It doesn't cost us a lot more to stub more of boot options here. Maybe we should just copying the options defined in https://github.com/NixOS/nixpkgs/blob/80bdc1e5ce51f56b19791b52b2901187931f5353/nixos/modules/system/boot/kernel.nix) ? I guess importing that upstream file directly would have been a problem ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think importing it directly wouldn't give us much benefit anyway as a lot of those options don't really make sense in our context, and we'd still need to define extra stubs to make everything work properly.

kernelModules = lib.mkOption {
type = attrNamesToTrue;
default = { };
description = ''
The set of kernel modules to be loaded in the second stage of
the boot process.

${modulesTypeDesc}
'';
apply = mods: lib.attrNames (lib.filterAttrs (_: v: v) mods);
};

kernelPackages = lib.mkOption {
type = lib.types.raw;
default = {
kernel.version = "stub";
};
description = "Stub kernel packages for compatibility; not actively used in system-manager.";
};
};

# nixos/modules/services/system/userborn.nix still depends on activation scripts
Expand Down Expand Up @@ -66,4 +99,35 @@
};
};
};

config = {
# Create /etc/modules-load.d/system-manager.conf, which is read by
# systemd-modules-load.service to load required kernel modules.
environment.etc = lib.mkIf (config.boot.kernelModules != [ ]) {
"modules-load.d/system-manager.conf".source = kernelModulesConf;
Comment thread
yuxqiu marked this conversation as resolved.
};

systemd.services = {
systemd-modules-load = lib.mkIf (config.boot.kernelModules != [ ]) {
overrideStrategy = "asDropin";
wantedBy = [
"system-manager.target"
"multi-user.target"
];
restartTriggers = [ kernelModulesConf ];
serviceConfig = {
SuccessExitStatus = "0 1";
};
};

systemd-sysctl = lib.mkIf (config.environment.etc ? "sysctl.d/60-nixos.conf") {
overrideStrategy = "asDropin";
wantedBy = [
"system-manager.target"
"multi-user.target"
];
restartTriggers = [ config.environment.etc."sysctl.d/60-nixos.conf".source ];
};
};
};
}
86 changes: 86 additions & 0 deletions testFlake/container-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,90 @@ in
machine.wait_for_unit("fail2ban.service")
'';
};

container-systemd-override-strategy = makeContainerTestFor "systemd-override-strategy" {
modules = [
(
{ pkgs, ... }:
let
etcOnlyUnit = pkgs.writeTextDir "etc/systemd/system/etc-only.service" ''
[Unit]
Description=Unit shipped from etc/systemd/system

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'touch /run/etc-only-service-started'
'';

asDropinBaseUnit = pkgs.writeTextDir "etc/systemd/system/as-dropin.service" ''
[Unit]
Description=Base unit from package (asDropin)

[Service]
Type=oneshot
ExecStart=/bin/true
'';

asDropinIfExistsBaseUnit = pkgs.writeTextDir "lib/systemd/system/as-if-exists.service" ''
[Unit]
Description=Base unit from package (asDropinIfExists)

[Service]
Type=oneshot
ExecStart=/bin/true
'';
in
{
systemd.packages = [
etcOnlyUnit
asDropinBaseUnit
asDropinIfExistsBaseUnit
];

systemd.services = {
as-dropin = {
enable = true;
overrideStrategy = "asDropin";
description = "Override generated as explicit drop-in";
script = ''
echo "as-dropin override"
'';
};

as-if-exists = {
enable = true;
description = "Override generated as drop-in only if base unit exists";
script = ''
echo "as-if-exists override"
'';
};
};
}
)
];
testScriptFunction =
{ toplevel, hostPkgs, ... }:
''
start_all()
machine.wait_for_unit("multi-user.target")

machine.activate()
machine.wait_for_unit("system-manager.target")

with subtest("Unit file from package etc/systemd/system is copied"):
unit = machine.file("/etc/systemd/system/etc-only.service")
assert unit.exists, "etc-only.service should exist"
assert unit.is_symlink or unit.is_file, "etc-only.service should be a file or symlink"
machine.succeed("systemctl start etc-only.service")
machine.succeed("test -f /run/etc-only-service-started")

with subtest("overrideStrategy=asDropin produces a drop-in"):
machine.succeed("test -L /etc/systemd/system/as-dropin.service")
machine.succeed("test -L /etc/systemd/system/as-dropin.service.d/overrides.conf")

with subtest("default overrideStrategy behaves as asDropinIfExists"):
machine.succeed("test -L /etc/systemd/system/as-if-exists.service")
machine.succeed("test -L /etc/systemd/system/as-if-exists.service.d/overrides.conf")
'';
};
}
86 changes: 86 additions & 0 deletions testFlake/vm-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1046,3 +1046,89 @@ forEachUbuntuImage "example" {
print("SSH key-based sops decryption test passed!")
'';
}

//

# Test that boot.kernel.sysctl and boot.kernelModules are applied at runtime
forEachUbuntuImage "boot-config" {
modules = [
(
{ ... }:
{
boot.kernel.sysctl = {
"net.ipv4.ip_forward" = 1;
"vm.swappiness" = 10;
};
boot.kernelModules = [ "veth" ];
}
)
];
extraPathsToRegister =
let
emptyConfig = system-manager.lib.makeSystemConfig {
modules = [
{
nixpkgs.hostPlatform = system;
}
];
};
in
[ emptyConfig ];
testScriptFunction =
{ toplevel, hostPkgs, ... }:
let
emptyConfig = system-manager.lib.makeSystemConfig {
modules = [
{
nixpkgs.hostPlatform = system;
}
];
};
in
''
start_all()
vm.wait_for_unit("default.target")

# Activate empty config: modules-load.d config should not be created
${system-manager.lib.activateProfileSnippet {
node = "vm";
profile = emptyConfig;
}}
vm.wait_for_unit("system-manager.target")

vm.fail("test -f /etc/modules-load.d/system-manager.conf")
vm.fail("test -d /etc/systemd/system/systemd-modules-load.service.d")
# sysctl drop-in exist even without explicit config (upstream defaults)
vm.succeed("test -e /etc/systemd/system/systemd-sysctl.service.d/overrides.conf")

print(vm.succeed("systemctl --no-pager status systemd-modules-load.service || true"))

# Activate with kernel modules: config should exist
${system-manager.lib.activateProfileSnippet {
node = "vm";
profile = toplevel;
}}
vm.wait_for_unit("system-manager.target")

vm.succeed("test -f /etc/modules-load.d/system-manager.conf")
vm.succeed("grep -q veth /etc/modules-load.d/system-manager.conf")
vm.succeed("test -e /etc/systemd/system/systemd-modules-load.service.d/overrides.conf")

# Verify sysctl config file
vm.succeed("test -f /etc/sysctl.d/60-nixos.conf")
vm.succeed("grep -q net.ipv4.ip_forward /etc/sysctl.d/60-nixos.conf")
vm.succeed("grep -q vm.swappiness /etc/sysctl.d/60-nixos.conf")
vm.succeed("test -e /etc/systemd/system/systemd-sysctl.service.d/overrides.conf")

ip_forward = vm.succeed("sysctl -n net.ipv4.ip_forward").strip()
assert ip_forward == "1", f"Expected net.ipv4.ip_forward=1, got {ip_forward}"

swappiness = vm.succeed("sysctl -n vm.swappiness").strip()
assert swappiness == "10", f"Expected vm.swappiness=10, got {swappiness}"

# Debug output to surface module-load status in CI logs
print(vm.succeed("systemctl --no-pager status systemd-modules-load.service || true"))

vm.succeed("lsmod | grep -q veth")
'';
}