Skip to content

Commit 63dfed5

Browse files
authored
Merge pull request #4881 from eedgar/paramiko-789-workaround
Close proxycommand socket when its not needed anymore and resolve ssh zombies
2 parents 8c592e1 + 9bd10f7 commit 63dfed5

File tree

3 files changed

+39
-3
lines changed

3 files changed

+39
-3
lines changed

CHANGELOG.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ Added
2020

2121
Changed
2222
~~~~~~~
23-
2423
* Install pack with the latest tag version if it exists when branch is not specialized.
2524
(improvement) #4743
2625
* Implement "continue" engine command to orquesta workflow. (improvement) #4740
@@ -65,6 +64,7 @@ Changed
6564
Fixed
6665
~~~~~
6766

67+
* Fix ssh zombies when using ProxyCommand from ssh config #4881 [Eric Edgar]
6868
* Fix rbac with execution view where the rbac is unable to verify the pack or uid of the execution
6969
because it was not returned from the action execution db. This would result in an internal server
7070
error when trying to view the results of a single execution.

st2actions/tests/unit/test_paramiko_ssh.py

+29
Original file line numberDiff line numberDiff line change
@@ -802,3 +802,32 @@ def test_use_ssh_config_port_value_provided_in_the_config(self, mock_sshclient):
802802

803803
call_kwargs = mock_client.connect.call_args[1]
804804
self.assertEqual(call_kwargs['port'], 9999)
805+
806+
@patch.object(ParamikoSSHClient, '_is_key_file_needs_passphrase',
807+
MagicMock(return_value=False))
808+
def test_socket_closed(self):
809+
conn_params = {'hostname': 'dummy.host.org',
810+
'username': 'ubuntu',
811+
'password': 'pass',
812+
'timeout': '600'}
813+
ssh_client = ParamikoSSHClient(**conn_params)
814+
815+
# Make sure .close() doesn't actually call anything real
816+
ssh_client.client = Mock()
817+
ssh_client.sftp_client = None
818+
ssh_client.bastion_client = None
819+
820+
ssh_client.socket = Mock()
821+
822+
# Make sure we havent called any close methods at this point
823+
# TODO: Replace these with .assert_not_called() once it's Python 3.6+ only
824+
self.assertEqual(ssh_client.socket.process.kill.call_count, 0)
825+
self.assertEqual(ssh_client.socket.process.poll.call_count, 0)
826+
827+
# Call the function that has changed
828+
ssh_client.close()
829+
830+
# Make sure we have called kill and poll
831+
# TODO: Replace these with .assert_called_once() once it's Python 3.6+ only
832+
self.assertEqual(ssh_client.socket.process.kill.call_count, 1)
833+
self.assertEqual(ssh_client.socket.process.poll.call_count, 1)

st2common/st2common/runners/paramiko_ssh.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def __init__(self, hostname, port=DEFAULT_SSH_PORT, username=None, password=None
123123

124124
self.bastion_client = None
125125
self.bastion_socket = None
126+
self.socket = None
126127

127128
def connect(self):
128129
"""
@@ -455,6 +456,12 @@ def close(self):
455456

456457
self.client.close()
457458

459+
if self.socket:
460+
self.logger.debug('Closing proxycommand socket connection')
461+
# https://github.com/paramiko/paramiko/issues/789 Avoid zombie ssh processes
462+
self.socket.process.kill()
463+
self.socket.process.poll()
464+
458465
if self.sftp_client:
459466
self.sftp_client.close()
460467

@@ -698,8 +705,8 @@ def _connect(self, host, socket=None):
698705
'_username': self.username, '_timeout': self.timeout}
699706
self.logger.debug('Connecting to server', extra=extra)
700707

701-
socket = socket or ssh_config_file_info.get('sock', None)
702-
if socket:
708+
self.socket = socket or ssh_config_file_info.get('sock', None)
709+
if self.socket:
703710
conninfo['sock'] = socket
704711

705712
client = paramiko.SSHClient()

0 commit comments

Comments
 (0)