diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py index 0644d4425c8b..e7782b545828 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py @@ -1,6 +1,7 @@ import json import logging import os +import re import sys import textwrap import uuid @@ -47,9 +48,6 @@ SWITCH_TO_CONFIGURATION_CMD_PREFIX: Final = [ "NIXOS_NO_CHECK", "--collect", "--no-ask-password", - "--wait", - "--verbose", - "--output=cat", "--quiet", "--service-type=exec", "--unit=nixos-rebuild-switch-to-configuration", @@ -734,6 +732,10 @@ def switch_to_configuration( cmd = [] elif os.environ.get("NIXOS_REBUILD_NO_SYSTEMD_RUN"): cmd = [] + elif _systemd_run_supports_output_cat(target_host): + cmd = [*cmd, "--wait", "--verbose", "--output=cat"] + else: + cmd = [*cmd, "--pipe"] run_wrapper( [*cmd, path_to_config / "bin/switch-to-configuration", str(action)], @@ -753,6 +755,23 @@ def switch_to_configuration( ) +# TODO: remove this after release-27.05 and assume systemd 261+ +def _systemd_run_supports_output_cat(target_host: Remote | None) -> bool: + """Check whether the target's systemd-run supports --output=cat (systemd 261+).""" + try: + result = run_wrapper( + ["systemd-run", "--version"], + remote=target_host, + capture_output=True, + ) + except CalledProcessError: + logger.debug("systemd-run version detection failed, assuming <261") + return False + + match = re.search(r"^systemd (\d+)(?:\D|$)", result.stdout, re.MULTILINE) + return match is not None and int(match.group(1)) >= 261 + + def upgrade_channels( all_channels: bool = False, elevate: Elevator = NO_ELEVATOR, diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py index 825c8e41d3d2..c8f70d9cf1e1 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_main.py @@ -227,6 +227,8 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: return CompletedProcess([], 0, "nixpkgs-rev") elif args[0] == "nix-build": return CompletedProcess([], 0, str(config_path)) + elif "systemd-run" in args and "--version" in args: + return CompletedProcess([], 0, "systemd 261 (261.2)") else: return CompletedProcess([], 0) @@ -234,7 +236,7 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: nr.execute(["nixos-rebuild", "boot", "--no-flake", "-vvv", "--no-reexec"]) - assert mock_run.call_count == 8 + assert mock_run.call_count == 9 mock_run.assert_has_calls( [ call( @@ -289,9 +291,18 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None: check=False, **DEFAULT_RUN_KWARGS, ), + call( + ["systemd-run", "--version"], + check=True, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + "--wait", + "--verbose", + "--output=cat", config_path / "bin/switch-to-configuration", "boot", ], @@ -571,6 +582,8 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: return CompletedProcess([], 0, str(config_path)) elif args[0] == "nix-instantiate": return CompletedProcess([], 1) + elif "systemd-run" in args and "--version" in args: + return CompletedProcess([], 0, "systemd 258 (258.2)") else: return CompletedProcess([], 0) @@ -593,7 +606,7 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: ] ) - assert mock_run.call_count == 5 + assert mock_run.call_count == 6 mock_run.assert_has_calls( [ call( @@ -637,6 +650,12 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: check=False, **DEFAULT_RUN_KWARGS, ), + call( + ["systemd-run", "--version"], + check=True, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "sudo", @@ -644,6 +663,7 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None: "-i", "NIXOS_INSTALL_BOOTLOADER=1", *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + "--pipe", config_path / "bin/switch-to-configuration", "switch", ], @@ -685,6 +705,8 @@ def test_execute_nix_switch_build_target_host_custom_profile( return CompletedProcess([], 0, "/tmp/tmpdir") elif args[0] == "ssh" and "readlink" in args: return CompletedProcess([], 0, str(config_path)) + elif "systemd-run" in args and "--version" in args: + return CompletedProcess([], 0, "systemd 258 (258.2)") else: return CompletedProcess([], 0) @@ -712,7 +734,7 @@ def test_execute_nix_switch_build_target_host_custom_profile( ] ) - assert mock_run.call_count == 13 + assert mock_run.call_count == 14 mock_run.assert_has_calls( [ call( @@ -901,6 +923,23 @@ def test_execute_nix_switch_build_target_host_custom_profile( check=False, **DEFAULT_RUN_KWARGS, ), + call( + [ + "ssh", + *nr.process.SSH_DEFAULT_OPTS, + "user@target-host", + "--", + "/bin/sh", + "-c", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", + "sh", + "systemd-run", + "--version", + ], + check=True, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "ssh", @@ -913,6 +952,7 @@ def test_execute_nix_switch_build_target_host_custom_profile( """'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + "--pipe", str(config_path / "bin/switch-to-configuration"), "switch", ], @@ -944,6 +984,8 @@ def test_execute_nix_switch_flake_target_host( return CompletedProcess([], 0, str(config_path)) elif args[0] == "nix-instantiate": return CompletedProcess([], 1) + elif "systemd-run" in args and "--version" in args: + return CompletedProcess([], 0, "systemd 258 (258.2)") else: return CompletedProcess([], 0) @@ -962,7 +1004,7 @@ def test_execute_nix_switch_flake_target_host( ] ) - assert mock_run.call_count == 6 + assert mock_run.call_count == 7 mock_run.assert_has_calls( [ call( @@ -1027,6 +1069,23 @@ def test_execute_nix_switch_flake_target_host( check=False, **DEFAULT_RUN_KWARGS, ), + call( + [ + "ssh", + *nr.process.SSH_DEFAULT_OPTS, + "user@localhost", + "--", + "/bin/sh", + "-c", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", + "sh", + "systemd-run", + "--version", + ], + check=True, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "ssh", @@ -1039,6 +1098,7 @@ def test_execute_nix_switch_flake_target_host( """'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + "--pipe", str(config_path / "bin/switch-to-configuration"), "switch", ], @@ -1072,6 +1132,8 @@ def test_execute_nix_switch_flake_build_host( return CompletedProcess([], 0, str(config_path)) elif args[0] == "nix-instantiate": return CompletedProcess([], 1) + elif "systemd-run" in args and "--version" in args: + return CompletedProcess([], 0, "systemd 258 (258.2)") else: return CompletedProcess([], 0) @@ -1089,7 +1151,7 @@ def test_execute_nix_switch_flake_build_host( ] ) - assert mock_run.call_count == 8 + assert mock_run.call_count == 9 mock_run.assert_has_calls( [ call( @@ -1164,9 +1226,16 @@ def test_execute_nix_switch_flake_build_host( check=False, **DEFAULT_RUN_KWARGS, ), + call( + ["systemd-run", "--version"], + check=True, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + "--pipe", config_path / "bin/switch-to-configuration", "switch", ], @@ -1527,7 +1596,7 @@ def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None: ) # --store-path skips build and write_version_suffix, so only activation calls - assert mock_run.call_count == 4 + assert mock_run.call_count == 5 mock_run.assert_has_calls( [ call( @@ -1552,9 +1621,16 @@ def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None: check=False, **DEFAULT_RUN_KWARGS, ), + call( + ["systemd-run", "--version"], + check=True, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + "--pipe", config_path / "bin/switch-to-configuration", "switch", ], @@ -1600,9 +1676,15 @@ def test_execute_switch_store_path_target_host( ) # --store-path skips build and write_version_suffix, so only copy/activation calls - assert mock_run.call_count == 6 + assert mock_run.call_count == 7 mock_run.assert_has_calls( [ + call( + ["nix-instantiate", "--find-file", "nixos-system"], + check=False, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( ["nix-copy-closure", "--to", "user@remote-host", config_path], check=True, @@ -1662,6 +1744,23 @@ def test_execute_switch_store_path_target_host( check=False, **DEFAULT_RUN_KWARGS, ), + call( + [ + "ssh", + *nr.process.SSH_DEFAULT_OPTS, + "user@remote-host", + "--", + "/bin/sh", + "-c", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", + "sh", + "systemd-run", + "--version", + ], + check=True, + capture_output=True, + **DEFAULT_RUN_KWARGS, + ), call( [ "ssh", @@ -1674,6 +1773,7 @@ def test_execute_switch_store_path_target_host( """'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""", "sh", *nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + "--pipe", str(config_path / "bin/switch-to-configuration"), "switch", ], diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py index e65531090a2e..fb6d2fcdcb69 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_nix.py @@ -957,9 +957,16 @@ def test_switch_to_configuration_without_systemd_run_env_var( ) +@patch( + get_qualified_name(n._systemd_run_supports_output_cat, n), + autospec=True, + return_value=True, +) @patch(get_qualified_name(n.run_wrapper, n), autospec=True) def test_switch_to_configuration_with_systemd_run( - mock_run: Mock, monkeypatch: MonkeyPatch + mock_run: Mock, + mock_supports_output_cat: Mock, + monkeypatch: MonkeyPatch, ) -> None: profile_path = Path("/path/to/profile") config_path = Path("/path/to/config") @@ -979,6 +986,9 @@ def test_switch_to_configuration_with_systemd_run( mock_run.assert_called_with( [ *n.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + "--wait", + "--verbose", + "--output=cat", profile_path / "bin/switch-to-configuration", "switch", ], @@ -992,6 +1002,7 @@ def test_switch_to_configuration_with_systemd_run( stdout=sys.stderr, ) + mock_supports_output_cat.return_value = False target_host = m.Remote("user@localhost", [], "ssh") with monkeypatch.context() as mp: mp.setenv("LOCALE_ARCHIVE", "/path/to/locale") @@ -1009,6 +1020,7 @@ def test_switch_to_configuration_with_systemd_run( mock_run.assert_called_with( [ *n.SWITCH_TO_CONFIGURATION_CMD_PREFIX, + "--pipe", config_path / "specialisation/special/bin/switch-to-configuration", "test", ], @@ -1023,6 +1035,33 @@ def test_switch_to_configuration_with_systemd_run( ) +@pytest.mark.parametrize( + ("version_output", "expected"), + [ + ("systemd 260 (260.1)\n", False), + ( + textwrap.dedent("""\ + systemd 261 (261.2) + +PAM +AUDIT -SELINUX +APPARMOR +IMA +IPE +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 +KMOD +LIBCRYPTSETUP +LIBCRYPTSETUP_PLUGINS +LIBFDISK +PCRE2 +PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD +BPF_FRAMEWORK -BTF -XKBCOMMON +UTMP +LIBARCHIVE + """), + True, + ), + ("systemd 270 (270.2)\n", True), + ("unexpected output\n", False), + ], +) +@patch(get_qualified_name(n.run_wrapper, n), autospec=True) +def test_systemd_run_supports_output_cat( + mock_run: Mock, version_output: str, expected: bool +) -> None: + mock_run.return_value = CompletedProcess([], 0, stdout=version_output) + + assert n._systemd_run_supports_output_cat(None) is expected + mock_run.assert_called_once_with( + ["systemd-run", "--version"], remote=None, capture_output=True + ) + + @patch("os.geteuid", autospec=True, return_value=1000) @patch(get_qualified_name(n.run_wrapper, n), autospec=True) def test_upgrade_channels(mock_run: Mock, mock_geteuid: Mock, tmpdir: Path) -> None: