diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd index 218eaa8d395f..305c39a2c2ac 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd +++ b/pkgs/by-name/ni/nixos-rebuild-ng/nixos-rebuild.8.scd @@ -379,6 +379,11 @@ NIX_PATH NIX_SSHOPTS Additional options to be passed to ssh on the command line. +NIXOS_REBUILD_SSH_DEFAULT_OPTS + Replaces the built-in default ssh options (connection sharing via a + private _ControlMaster_ that is closed on exit). If empty, no + default options are added. + NIX_SUDOOPTS Additional options to be passed to sudo on the command line. 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 4fbe7fbe2b9d..5a4be4d1a557 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 @@ -26,7 +26,7 @@ from .models import ( Profile, Remote, ) -from .process import SSH_DEFAULT_OPTS, run_wrapper +from .process import run_wrapper, ssh_default_opts from .utils import Args, dict_to_flags FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"] @@ -193,7 +193,7 @@ def copy_closure( Also supports copying a closure from a remote to another remote.""" sshopts = os.getenv("NIX_SSHOPTS", "") - env = {"NIX_SSHOPTS": " ".join(filter(lambda x: x, [sshopts, *SSH_DEFAULT_OPTS]))} + env = {"NIX_SSHOPTS": " ".join(filter(lambda x: x, [sshopts, *ssh_default_opts()]))} def nix_copy_closure(host: Remote, to: bool) -> None: run_wrapper( diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py index 7ba809d4cb9c..3ae6afa9c68c 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py @@ -31,6 +31,14 @@ SSH_DEFAULT_OPTS: Final = [ ] +def ssh_default_opts() -> list[str]: + "Default ssh options appended after NIX_SSHOPTS." + env = os.getenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS") + if env is None: + return SSH_DEFAULT_OPTS + return shlex.split(env) + + @dataclass(frozen=True) class Remote: host: str @@ -133,7 +141,7 @@ def run_wrapper( ssh_args: list[Arg] = [ "ssh", *remote.opts, - *SSH_DEFAULT_OPTS, + *ssh_default_opts(), remote.ssh_host(), "--", *[_quote_remote_arg(a) for a in remote_run_args], @@ -282,7 +290,7 @@ def _kill_long_running_ssh_process(args: Args, remote: Remote) -> None: [ "ssh", *remote.opts, - *SSH_DEFAULT_OPTS, + *ssh_default_opts(), remote.ssh_host(), "--", "pkill", 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 1cdccc9a874e..848a09bb742b 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 @@ -275,6 +275,17 @@ def test_copy_closure(monkeypatch: MonkeyPatch) -> None: }, ) + # NIXOS_REBUILD_SSH_DEFAULT_OPTS replaces the ControlMaster defaults + monkeypatch.setenv("NIX_SSHOPTS", "-oControlPath=/run/user/1000/%C") + monkeypatch.setenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS", "") + with patch(get_qualified_name(n.run_wrapper, n), autospec=True) as mock_run: + n.copy_closure(closure, target_host) + mock_run.assert_called_with( + ["nix-copy-closure", "--to", "user@target.host", closure], + append_local_env={"NIX_SSHOPTS": "-oControlPath=/run/user/1000/%C"}, + ) + monkeypatch.delenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS") + monkeypatch.setenv("NIX_SSHOPTS", "--ssh build-target-opt") env = {"NIX_SSHOPTS": " ".join(["--ssh build-target-opt", *p.SSH_DEFAULT_OPTS])} with patch(get_qualified_name(n.run_wrapper, n), autospec=True) as mock_run: diff --git a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py index 2abdd26ebe07..c8f1ec9126e9 100644 --- a/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py +++ b/pkgs/by-name/ni/nixos-rebuild-ng/src/tests/test_process.py @@ -234,6 +234,52 @@ def test_remote_from_name(monkeypatch: MonkeyPatch) -> None: ) +def test_ssh_default_opts(monkeypatch: MonkeyPatch) -> None: + monkeypatch.delenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS", raising=False) + assert p.ssh_default_opts() == p.SSH_DEFAULT_OPTS + + monkeypatch.setenv( + "NIXOS_REBUILD_SSH_DEFAULT_OPTS", "-o ControlPath=/run/user/1000/%C" + ) + assert p.ssh_default_opts() == ["-o", "ControlPath=/run/user/1000/%C"] + + monkeypatch.setenv("NIXOS_REBUILD_SSH_DEFAULT_OPTS", "") + assert p.ssh_default_opts() == [] + + +@patch.dict( + p.os.environ, + {"PATH": "/path/to/bin", "NIXOS_REBUILD_SSH_DEFAULT_OPTS": ""}, + clear=True, +) +@patch("subprocess.run", autospec=True) +def test_run_wrapper_ssh_default_opts_override(mock_run: Any) -> None: + p.run_wrapper( + ["test"], + check=True, + remote=m.Remote("user@localhost", ["-p", "2222"], "ssh"), + ) + mock_run.assert_called_with( + [ + "ssh", + "-p", + "2222", + "user@localhost", + "--", + "/bin/sh", + "-c", + """'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""", + "sh", + "test", + ], + check=True, + text=True, + errors="surrogateescape", + env=None, + input=None, + ) + + def test_ssh_host() -> None: ssh_remotes = { "user@[fe80::1%25eth0]": "user@fe80::1%eth0",