From 1e262f148be28f5b70eab8d51d41a36508f557b6 Mon Sep 17 00:00:00 2001 From: Jared Baur Date: Sun, 14 Sep 2025 14:05:30 -0700 Subject: [PATCH 1/6] switch-to-configuration-ng: fix typo --- pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs index f4d339ccf60e..771aa243e055 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs @@ -823,7 +823,7 @@ fn path_to_unit_name(bin_path: &Path, path: &str) -> String { }; let Ok(unit) = String::from_utf8(output.stdout) else { - eprintln!("Unable to convert systemd-espape output to valid UTF-8"); + eprintln!("Unable to convert systemd-escape output to valid UTF-8"); die(); }; From 2d12aa1ddbaaa7dfd1f6c53a9c9f22fcd96faf45 Mon Sep 17 00:00:00 2001 From: Jared Baur Date: Sun, 14 Sep 2025 14:05:50 -0700 Subject: [PATCH 2/6] switch-to-configuration-ng: ignore units not explicitly written by NixOS --- .../src/src/main.rs | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs index 771aa243e055..f9247b0daeb8 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs @@ -201,6 +201,7 @@ fn required_env(var: &str) -> anyhow::Result { struct UnitState { state: String, substate: String, + managed_by_nixos: bool, } // Asks the currently running systemd instance via dbus which units are active. Returns a hash @@ -222,29 +223,44 @@ fn get_active_units( active_state, sub_state, following, - _unit_path, + unit_path, _job_id, _job_type, _job_path, )| { + let unit_managed_by_nixos = systemd_manager + .connection + .with_proxy( + "org.freedesktop.systemd1", + unit_path, + Duration::from_millis(5000), + ) + .get("org.freedesktop.systemd1.Unit", "FragmentPath") + .map(|fragment_path: String| fragment_path.starts_with("/etc/systemd/system")) + .unwrap_or_default(); + if following.is_empty() && active_state != "inactive" { - Some((id, active_state, sub_state)) + Some((id, active_state, sub_state, unit_managed_by_nixos)) } else { None } }, ) - .fold(HashMap::new(), |mut acc, (id, active_state, sub_state)| { - acc.insert( - id, - UnitState { - state: active_state, - substate: sub_state, - }, - ); + .fold( + HashMap::new(), + |mut acc, (id, active_state, sub_state, managed_by_nixos)| { + acc.insert( + id, + UnitState { + state: active_state, + substate: sub_state, + managed_by_nixos, + }, + ); - acc - })) + acc + }, + )) } // This function takes a single ini file that specified systemd configuration like unit @@ -1189,6 +1205,12 @@ won't take effect until you reboot the system. .context("Invalid regex for matching systemd unit names")?; for (unit, unit_state) in ¤t_active_units { + // Don't touch units not explicitly written by NixOS (e.g. units created by generators in + // /run/systemd/generator*) + if !unit_state.managed_by_nixos { + continue; + } + let current_unit_file = Path::new("/etc/systemd/system").join(unit); let new_unit_file = toplevel.join("etc/systemd/system").join(unit); From 477b8796f9eabe769f4a07a63731345f679416d2 Mon Sep 17 00:00:00 2001 From: Jared Baur Date: Sun, 14 Sep 2025 14:06:20 -0700 Subject: [PATCH 3/6] nixosTests.switchTest: assert switch-to-configuration ignores generated units --- nixos/tests/switch-test.nix | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix index 545cbe25f167..97ccc131224c 100644 --- a/nixos/tests/switch-test.nix +++ b/nixos/tests/switch-test.nix @@ -627,6 +627,17 @@ in (pkgs.writeText "dbus-reload-dummy" "dbus reload dummy") ]; }; + + generators.configuration = + { lib, pkgs, ... }: + { + systemd.generators.simple-generator = pkgs.writeShellScript "simple-generator" '' + ${lib.getExe' pkgs.coreutils "cat"} >$1/simple-generated.service < Date: Sun, 14 Sep 2025 16:07:13 -0700 Subject: [PATCH 4/6] switch-to-configuration-ng: clean up unit state If we obtain a "Proxy" to the unit using the unit DBus object path, we can use any of the DBus APIs later on. --- .../src/src/main.rs | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs index f9247b0daeb8..183d2648d2b0 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs @@ -197,18 +197,17 @@ fn required_env(var: &str) -> anyhow::Result { std::env::var(var).with_context(|| format!("missing required environment variable ${var}")) } -#[derive(Debug)] -struct UnitState { +struct UnitState<'a, 'b> { state: String, substate: String, - managed_by_nixos: bool, + proxy: Proxy<'a, &'b LocalConnection>, } // Asks the currently running systemd instance via dbus which units are active. Returns a hash // where the key is the name of each unit and the value a hash of load, state, substate. -fn get_active_units( - systemd_manager: &Proxy<'_, &LocalConnection>, -) -> Result> { +fn get_active_units<'a, 'b>( + systemd_manager: &Proxy<'a, &'b LocalConnection>, +) -> Result>> { let units = systemd_manager .list_units_by_patterns(Vec::new(), Vec::new()) .context("Failed to list systemd units")?; @@ -220,47 +219,39 @@ fn get_active_units( id, _description, _load_state, - active_state, - sub_state, + state, + substate, following, unit_path, _job_id, _job_type, _job_path, )| { - let unit_managed_by_nixos = systemd_manager - .connection - .with_proxy( - "org.freedesktop.systemd1", - unit_path, - Duration::from_millis(5000), - ) - .get("org.freedesktop.systemd1.Unit", "FragmentPath") - .map(|fragment_path: String| fragment_path.starts_with("/etc/systemd/system")) - .unwrap_or_default(); + let proxy = systemd_manager.connection.with_proxy( + "org.freedesktop.systemd1", + unit_path, + Duration::from_millis(5000), + ); - if following.is_empty() && active_state != "inactive" { - Some((id, active_state, sub_state, unit_managed_by_nixos)) + if following.is_empty() && state != "inactive" { + Some(( + id, + UnitState { + state, + substate, + proxy, + }, + )) } else { None } }, ) - .fold( - HashMap::new(), - |mut acc, (id, active_state, sub_state, managed_by_nixos)| { - acc.insert( - id, - UnitState { - state: active_state, - substate: sub_state, - managed_by_nixos, - }, - ); + .fold(HashMap::new(), |mut acc, (id, unit_state)| { + acc.insert(id, unit_state); - acc - }, - )) + acc + })) } // This function takes a single ini file that specified systemd configuration like unit @@ -1207,7 +1198,12 @@ won't take effect until you reboot the system. for (unit, unit_state) in ¤t_active_units { // Don't touch units not explicitly written by NixOS (e.g. units created by generators in // /run/systemd/generator*) - if !unit_state.managed_by_nixos { + if !unit_state + .proxy + .get("org.freedesktop.systemd1.Unit", "FragmentPath") + .map(|fragment_path: String| fragment_path.starts_with("/etc/systemd/system")) + .unwrap_or_default() + { continue; } From cd961a6683dce1cc60af501c3f55188bc03e1e96 Mon Sep 17 00:00:00 2001 From: Jared Baur Date: Sun, 14 Sep 2025 16:08:06 -0700 Subject: [PATCH 5/6] switch-to-configuration-ng: handle automounts in /etc/fstab It would be nicer to use DBus APIs in order to obtain unit information such as whether a unit is an automount unit, but in order to support the "dry-activate" subcommand to switch-to-configuration, we must obtain information without direct interaction with systemd and the DBus API it exposes. --- .../sw/switch-to-configuration-ng/src/src/main.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs index 183d2648d2b0..1472022d47d1 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs @@ -818,9 +818,12 @@ fn parse_fstab(fstab: impl BufRead) -> (HashMap, HashMap String { +fn path_to_unit_name(bin_path: &Path, path: &str, is_automount: bool) -> String { let Ok(output) = std::process::Command::new(bin_path.join("systemd-escape")) - .arg("--suffix=mount") + .arg(format!( + "--suffix={}", + if is_automount { "automount" } else { "mount" } + )) .arg("-p") .arg(path) .output() @@ -1337,8 +1340,10 @@ won't take effect until you reboot the system. .unwrap_or_default(); for (mountpoint, current_filesystem) in current_filesystems { + let is_automount = current_filesystem.options.contains("x-systemd.automount"); + // Use current version of systemctl binary before daemon is reexeced. - let unit = path_to_unit_name(¤t_system_bin, &mountpoint); + let unit = path_to_unit_name(¤t_system_bin, &mountpoint, is_automount); if let Some(new_filesystem) = new_filesystems.get(&mountpoint) { if current_filesystem.fs_type != new_filesystem.fs_type || current_filesystem.device != new_filesystem.device From fe6a73c0f7f29e3f6ae0bf791111e0674a4f82d1 Mon Sep 17 00:00:00 2001 From: Jared Baur Date: Sun, 14 Sep 2025 16:11:13 -0700 Subject: [PATCH 6/6] nixosTests.switchTest: assert automount units are handled appropriately --- nixos/tests/switch-test.nix | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix index 97ccc131224c..c01dbc90ec7a 100644 --- a/nixos/tests/switch-test.nix +++ b/nixos/tests/switch-test.nix @@ -91,6 +91,14 @@ in virtualisation.fileSystems."/".device = lib.mkForce "auto"; }; + automount.configuration = { + virtualisation.fileSystems."/testauto" = { + device = "tmpfs"; + fsType = "tmpfs"; + options = [ "x-systemd.automount" ]; + }; + }; + swap.configuration.swapDevices = lib.mkVMOverride [ { device = "/swapfile"; @@ -837,9 +845,21 @@ in assert_lacks(out, "\nrestarting the following units:") assert_lacks(out, "\nstarting the following units:") assert_lacks(out, "the following new units were started:") + # add an automount + out = switch_to_specialisation("${machine}", "automount") + assert_lacks(out, "stopping the following units:") + assert_lacks(out, "\nrestarting the following units:") + assert_lacks(out, "\nstarting the following units:") + assert_contains(out, "the following new units were started: testauto.automount\n") + # remove the automount + out = switch_to_specialisation("${machine}", "") + assert_contains(out, "stopping the following units: testauto.automount\n") + assert_lacks(out, "reloading the following units:") + assert_lacks(out, "\nrestarting the following units:") + assert_lacks(out, "\nstarting the following units:") + assert_lacks(out, "the following new units were started:") with subtest("swaps"): - switch_to_specialisation("${machine}", "") # add a swap out = switch_to_specialisation("${machine}", "swap") assert_lacks(out, "stopping the following units:")