nixos/switchable-system: introduce a standard pre-switch check to prevent switching under certain conditions

This commit introduces "switch inhibitors" which are derivations that
prevent a switch of a system to a new configuration if those derivations
don't have the same hash in both configurations.
This means that we can for instance add the systemd and dbus derivations
such that users will be instructed to reboot their system when those
derivations have changed instead of switching.

This feature should be used sparingly, but it can make NixOS more robust
by avoiding users switching to a configuration that can make their
system unstable (like major updates of systemd, or new versions of dbus
since the dbus and dbus-broker daemons cannot be restarted).

The user can still force the switch by setting an env var.
This commit is contained in:
r-vdp
2025-12-25 12:02:34 +02:00
parent 0738aa49a4
commit 93cda47a1f
3 changed files with 128 additions and 25 deletions
+8
View File
@@ -174,6 +174,10 @@ in
permissions = "u+rx,g+rx,o-rx";
};
system.switch.inhibitors = [
cfg.dbusPackage
];
systemd.services.dbus = {
aliases = [
# hack aiding to prevent dbus from restarting when switching from dbus-broker back to dbus
@@ -212,6 +216,10 @@ in
cfg.brokerPackage
];
system.switch.inhibitors = [
cfg.brokerPackage
];
# Just to be sure we don't restart through the unit alias
systemd.services.dbus.reloadIfChanged = true;
systemd.user.services.dbus.reloadIfChanged = true;
@@ -16,38 +16,129 @@
'')
];
options.system.switch.enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to include the capability to switch configurations.
options.system.switch = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to include the capability to switch configurations.
Disabling this makes the system unable to be reconfigured via `nixos-rebuild`.
Disabling this makes the system unable to be reconfigured via `nixos-rebuild`.
This is good for image based appliances where updates are handled
outside the image. Reducing features makes the image lighter and
slightly more secure.
'';
This is good for image based appliances where updates are handled
outside the image. Reducing features makes the image lighter and
slightly more secure.
'';
};
inhibitors = lib.mkOption {
type = lib.types.listOf lib.types.pathInStore;
default = [ ];
description = ''
List of derivations that will prevent switching into a configuration when
they change.
This can be manually overridden on the command line if required.
'';
};
};
config = lib.mkIf config.system.switch.enable {
# Use a subshell so we can source makeWrapper's setup hook without
# affecting the rest of activatableSystemBuilderCommands.
system.activatableSystemBuilderCommands = ''
(
source ${pkgs.buildPackages.makeWrapper}/nix-support/setup-hook
system = {
activatableSystemBuilderCommands = ''
(
source ${pkgs.buildPackages.makeWrapper}/nix-support/setup-hook
mkdir $out/bin
ln -sf ${lib.getExe pkgs.switch-to-configuration-ng} $out/bin/switch-to-configuration
wrapProgram $out/bin/switch-to-configuration \
--set OUT $out \
--set TOPLEVEL ''${!toplevelVar} \
--set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \
--set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \
--set PRE_SWITCH_CHECK ${lib.escapeShellArg config.system.preSwitchChecksScript} \
--set LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \
--set SYSTEMD ${config.systemd.package}
)
'';
mkdir $out/bin
ln -sf ${lib.getExe pkgs.switch-to-configuration-ng} $out/bin/switch-to-configuration
wrapProgram $out/bin/switch-to-configuration \
--set OUT $out \
--set TOPLEVEL ''${!toplevelVar} \
--set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \
--set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \
--set PRE_SWITCH_CHECK ${lib.escapeShellArg config.system.preSwitchChecksScript} \
--set LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \
--set SYSTEMD ${config.systemd.package}
)
'';
systemBuilderCommands = ''
ln -s ${config.system.build.inhibitSwitch} $out/switch-inhibitors
'';
build.inhibitSwitch = pkgs.writeTextFile {
name = "switch-inhibitors";
text = lib.concatMapStringsSep "\n" (drv: drv.outPath) config.system.switch.inhibitors;
};
preSwitchChecks.switchInhibitors =
let
realpath = lib.getExe' pkgs.coreutils "realpath";
sha256sum = lib.getExe' pkgs.coreutils "sha256sum";
diff = lib.getExe' pkgs.diffutils "diff";
in
# bash
''
incoming="''${1-}"
action="''${2-}"
if [ "$action" == "boot" ]; then
echo "Not checking switch inhibitors (action = $action)"
exit
fi
echo -n "Checking switch inhibitors..."
booted_inhibitors="$(${realpath} /run/booted-system)/switch-inhibitors"
booted_inhibitors_sha="$(
if [ -f "$booted_inhibitors" ]; then
${sha256sum} - < "$booted_inhibitors"
else
echo 'none'
fi
)"
if [ "$booted_inhibitors_sha" == "none" ]; then
echo
echo "The previous configuration did not specify switch inhibitors, nothing to check."
exit
fi
new_inhibitors="$(${realpath} "$incoming")/switch-inhibitors"
new_inhibitors_sha="$(
if [ -f "$new_inhibitors" ]; then
${sha256sum} - < "$new_inhibitors"
else
echo 'none'
fi
)"
if [ "$new_inhibitors_sha" == "none" ]; then
echo
echo "The new configuration does not specify switch inhibitors, nothing to check."
exit
fi
if [ "$new_inhibitors_sha" != "$booted_inhibitors_sha" ]; then
echo
echo "Found diff in switch inhibitors:"
echo
${diff} --color "$booted_inhibitors" "$new_inhibitors"
echo
echo "The new configuration contains changes to packages that were"
echo "listed as switch inhibitors."
echo
echo "If you really want to switch into this configuration directly, then"
echo "you can set NIXOS_NO_CHECK=1 to ignore these pre-switch checks."
echo
echo "WARNING: doing so might cause the switch to fail or your system to become unstable."
echo
exit 1
else
echo " done"
fi
'';
};
};
}
+4
View File
@@ -567,6 +567,10 @@ in
);
};
system.switch.inhibitors = [
cfg.package
];
environment.systemPackages = [ cfg.package ];
environment.etc =