Skip to content

Commit 5991f14

Browse files
Akm0dThomas Phipps
authored andcommitted
Allow ssh pre connection hook
1 parent 21d5cca commit 5991f14

File tree

4 files changed

+85
-0
lines changed

4 files changed

+85
-0
lines changed

changelog/66210.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow pre-connection scripts to be run on host before any ssh commands

salt/client/ssh/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,7 @@ def __init__(
10081008
self.fsclient = fsclient
10091009
self.context = {"master_opts": self.opts, "fileclient": self.fsclient}
10101010

1011+
self.ssh_pre_hook = kwargs.get("ssh_pre_hook", None)
10111012
self.ssh_pre_flight = kwargs.get("ssh_pre_flight", None)
10121013
self.ssh_pre_flight_args = kwargs.get("ssh_pre_flight_args", None)
10131014

@@ -1093,6 +1094,12 @@ def _escape_arg(self, arg):
10931094
return arg
10941095
return "".join(["\\" + char if re.match(r"\W", char) else char for char in arg])
10951096

1097+
def run_ssh_pre_hook(self):
1098+
"""
1099+
Run a pre_hook script on the host machine before running any ssh commands
1100+
"""
1101+
return self.shell.exec_cmd(self.ssh_pre_hook)
1102+
10961103
def run_ssh_pre_flight(self):
10971104
"""
10981105
Run our pre_flight script before running any ssh commands
@@ -1168,6 +1175,13 @@ def run(self, deploy_attempted=False):
11681175
stdout = stderr = ""
11691176
retcode = salt.defaults.exitcodes.EX_OK
11701177

1178+
if self.ssh_pre_hook:
1179+
stdout, stderr, retcode = self.run_ssh_pre_hook()
1180+
if retcode != salt.defaults.exitcodes.EX_OK:
1181+
log.error("Error running ssh_pre_hook script %s", self.ssh_pre_hook)
1182+
return stdout, stderr, retcode
1183+
log.info("Successfully ran the ssh_pre_hook script: %s", self.ssh_pre_hook)
1184+
11711185
if self.ssh_pre_flight:
11721186
if not self.opts.get("ssh_run_pre_flight", False) and self.check_thin_dir():
11731187
log.info(

salt/client/ssh/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def sanitize_kwargs(self, kwargs):
6565
("ssh_scan_timeout", int),
6666
("ssh_timeout", int),
6767
("ssh_log_file", str),
68+
("ssh_pre_hook", str),
6869
("raw_shell", bool),
6970
("refresh_cache", bool),
7071
("roster", str),

tests/pytests/unit/client/ssh/test_single.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,3 +834,72 @@ def test_ssh_single__cmd_str_sudo_passwd_user(opts):
834834
)
835835

836836
assert expected in cmd
837+
838+
839+
def test_run_ssh_pre_hook_success(opts, target, tmp_path):
840+
"""
841+
Test run_ssh_pre_hook when ssh_pre_hook is successful.
842+
"""
843+
target["ssh_pre_hook"] = "echo 'Pre-hook success'"
844+
single_instance = ssh.Single(opts, opts["argv"], "localhost", **target)
845+
mock_exec_cmd = MagicMock(return_value=("Output", "No errors", 0))
846+
with patch.object(single_instance.shell, "exec_cmd", mock_exec_cmd):
847+
result = single_instance.run_ssh_pre_hook()
848+
assert result == ("Output", "No errors", 0)
849+
850+
851+
def test_run_ssh_pre_hook_failure(opts, target):
852+
"""
853+
Test run_ssh_pre_hook when ssh_pre_hook fails.
854+
"""
855+
target["ssh_pre_hook"] = "echo 'Pre-hook failure'"
856+
single_instance = ssh.Single(opts, opts["argv"], "localhost", **target)
857+
mock_exec_cmd = MagicMock(return_value=("Error output", "Failed to execute", 1))
858+
with patch.object(single_instance.shell, "exec_cmd", mock_exec_cmd):
859+
result = single_instance.run_ssh_pre_hook()
860+
assert result == ("Error output", "Failed to execute", 1)
861+
862+
863+
def test_run_integration_with_pre_hook_success(opts, target):
864+
"""
865+
Test the run method integrates run_ssh_pre_hook and proceeds on success.
866+
"""
867+
target["ssh_pre_hook"] = "echo 'Pre-hook success'"
868+
target["ssh_pre_flight"] = None
869+
single_instance = ssh.Single(opts, opts["argv"], "localhost", **target)
870+
mock_pre_hook = MagicMock(return_value=("", "", 0))
871+
mock_cmd_block = MagicMock(return_value=("", "", 0))
872+
with patch.object(single_instance, "run_ssh_pre_hook", mock_pre_hook), patch.object(
873+
single_instance, "cmd_block", mock_cmd_block
874+
):
875+
stdout, stderr, retcode = single_instance.run()
876+
assert retcode == 0
877+
mock_pre_hook.assert_called_once()
878+
879+
880+
def test_run_integration_with_pre_hook_failure(opts, target):
881+
"""
882+
Test the run method handles pre_hook failure correctly and skips further steps.
883+
"""
884+
target["ssh_pre_hook"] = "echo 'Pre-hook failure'"
885+
target["ssh_pre_flight"] = None
886+
single_instance = ssh.Single(opts, opts["argv"], "localhost", **target)
887+
mock_pre_hook = MagicMock(return_value=("Error output", "Failed to execute", 1))
888+
with patch.object(single_instance, "run_ssh_pre_hook", mock_pre_hook):
889+
stdout, stderr, retcode = single_instance.run()
890+
assert retcode == 1
891+
assert "Failed to execute" in stderr
892+
mock_pre_hook.assert_called_once()
893+
894+
895+
def test_run_integration_with_no_pre_hook(opts, target):
896+
"""
897+
Test the run method succeeds with no ssh_pre_hook
898+
"""
899+
target["ssh_pre_hook"] = None
900+
target["ssh_pre_flight"] = None
901+
single_instance = ssh.Single(opts, opts["argv"], "localhost", **target)
902+
mock_cmd_block = MagicMock(return_value=("", "", 0))
903+
with patch.object(single_instance, "cmd_block", mock_cmd_block):
904+
stdout, stderr, retcode = single_instance.run()
905+
assert retcode == 0

0 commit comments

Comments
 (0)