-
Notifications
You must be signed in to change notification settings - Fork 8
Remote: Automatically manage known hosts keys #451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0cf2c65
41a42a0
8868a61
6dd12d0
28e97e9
ccf1586
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -93,9 +93,12 @@ def __init__( | |||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| self._ssh_host = config["ssh_host"] | ||||||||||||||||||||||||||||||||||||||||||||
| self._ssh_username = config["ssh_username"] | ||||||||||||||||||||||||||||||||||||||||||||
| self._ssh_known_hosts = os.path.abspath( | ||||||||||||||||||||||||||||||||||||||||||||
| os.path.expanduser(config["known_hosts"]) | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| if "known_hosts" in config: | ||||||||||||||||||||||||||||||||||||||||||||
| self._ssh_known_hosts = os.path.abspath( | ||||||||||||||||||||||||||||||||||||||||||||
| os.path.expanduser(config["known_hosts"]) | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||
| self._ssh_known_hosts = "" | ||||||||||||||||||||||||||||||||||||||||||||
| self._ssh_ask_for_password: Union[bool, str] = False | ||||||||||||||||||||||||||||||||||||||||||||
| self._ssh_key = ( | ||||||||||||||||||||||||||||||||||||||||||||
| os.path.abspath(os.path.expanduser(config["ssh_key"])) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -346,7 +349,10 @@ def _open_ssh_connection(self) -> paramiko.SSHClient: | |||||||||||||||||||||||||||||||||||||||||||
| paramiko.SSHClient: The SSH connection object. | ||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||
| ssh = paramiko.SSHClient() | ||||||||||||||||||||||||||||||||||||||||||||
| ssh.load_host_keys(self._ssh_known_hosts) | ||||||||||||||||||||||||||||||||||||||||||||
| if len(self._ssh_known_hosts) > 0: | ||||||||||||||||||||||||||||||||||||||||||||
| ssh.load_host_keys(self._ssh_known_hosts) | ||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||
| ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+352
to
+355
|
||||||||||||||||||||||||||||||||||||||||||||
| if len(self._ssh_known_hosts) > 0: | |
| ssh.load_host_keys(self._ssh_known_hosts) | |
| else: | |
| ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
| if len(self._ssh_known_hosts) > 0 and os.path.exists(self._ssh_known_hosts): | |
| ssh.load_host_keys(self._ssh_known_hosts) | |
| else: | |
| if len(self._ssh_known_hosts) > 0: | |
| warnings.warn( | |
| "Configured SSH known_hosts file does not exist: " | |
| + self._ssh_known_hosts | |
| + ". Host key verification will use WarningPolicy instead.", | |
| UserWarning, | |
| ) | |
| else: | |
| warnings.warn( | |
| "SSH known_hosts is not configured. Host key verification is " | |
| "disabled for unknown hosts and WarningPolicy will be used.", | |
| UserWarning, | |
| ) | |
| ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,14 @@ | ||||
| queue_type: REMOTE | ||||
| queue_primary: remote | ||||
| ssh_host: test.rebex.net | ||||
| ssh_username: demo | ||||
| known_hosts: ~/.ssh/known_hosts | ||||
|
||||
| known_hosts: ~/.ssh/known_hosts |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -151,36 +151,53 @@ def test_remote_command_continous_connection(self): | |||||||||||||||||||||||||||
| output = remote._adapter._execute_remote_command(command="pwd") | ||||||||||||||||||||||||||||
| self.assertEqual(output, "/\n") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_remote_command_individual_connections_hosts(self): | ||||||||||||||||||||||||||||
| path = os.path.dirname(os.path.abspath(__file__)) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||||||||||||||||||||||||||||
| remote._adapter._ssh_remote_path = path | ||||||||||||||||||||||||||||
| remote._adapter._open_ssh_connection() | ||||||||||||||||||||||||||||
| output = remote._adapter._execute_remote_command(command="pwd") | ||||||||||||||||||||||||||||
|
Comment on lines
+155
to
+159
|
||||||||||||||||||||||||||||
| self.assertEqual(output, "/\n") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
Comment on lines
+154
to
+161
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid opening a redundant SSH connection in the individual-connections host test. When _ssh_continous_connection is False, _execute_remote_command opens and closes its own connection. Calling _open_ssh_connection beforehand (Line 158) creates an unused connection and may leak resources. Apply this diff: def test_remote_command_individual_connections_hosts(self):
path = os.path.dirname(os.path.abspath(__file__))
remote = QueueAdapter(directory=os.path.join(path, "config/remote_rebex_hosts"))
remote._adapter._ssh_remote_path = path
- remote._adapter._open_ssh_connection()
output = remote._adapter._execute_remote_command(command="pwd")
self.assertEqual(output, "/\n")Optionally, make the same change in test_remote_command_individual_connections above (Lines 141–143) for consistency. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| def test_remote_command_continous_connection_hosts(self): | ||||||||||||||||||||||||||||
| path = os.path.dirname(os.path.abspath(__file__)) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||||||||||||||||||||||||||||
| remote._adapter._ssh_remote_path = path | ||||||||||||||||||||||||||||
| remote._adapter._ssh_continous_connection = True | ||||||||||||||||||||||||||||
| remote._adapter._open_ssh_connection() | ||||||||||||||||||||||||||||
| output = remote._adapter._execute_remote_command(command="pwd") | ||||||||||||||||||||||||||||
| self.assertEqual(output, "/\n") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_submit_job(self): | ||||||||||||||||||||||||||||
| path = os.path.dirname(os.path.abspath(__file__)) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex")) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||||||||||||||||||||||||||||
| remote._adapter._ssh_remote_path = path | ||||||||||||||||||||||||||||
| output = remote._adapter.submit_job(working_directory=os.path.join(path, "../../static/empty"), command="echo 1") | ||||||||||||||||||||||||||||
|
Comment on lines
171
to
175
|
||||||||||||||||||||||||||||
| self.assertEqual(output, 1) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_transferfile_individual_connections(self): | ||||||||||||||||||||||||||||
| path = os.path.dirname(os.path.abspath(__file__)) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex")) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||||||||||||||||||||||||||||
| remote._adapter._ssh_remote_path = path | ||||||||||||||||||||||||||||
| self.assertIsNone(remote._adapter.transfer_file(file="readme.txt", transfer_back=True)) | ||||||||||||||||||||||||||||
|
Comment on lines
178
to
182
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_transferfile_continous_connection(self): | ||||||||||||||||||||||||||||
| path = os.path.dirname(os.path.abspath(__file__)) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex")) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||||||||||||||||||||||||||||
| remote._adapter._ssh_remote_path = path | ||||||||||||||||||||||||||||
| remote._adapter._ssh_continous_connection = True | ||||||||||||||||||||||||||||
| self.assertIsNone(remote._adapter.transfer_file(file="readme.txt", transfer_back=True)) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_get_transport(self): | ||||||||||||||||||||||||||||
| path = os.path.dirname(os.path.abspath(__file__)) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex")) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||||||||||||||||||||||||||||
| self.assertIsNotNone(get_transport(remote._adapter._open_ssh_connection())) | ||||||||||||||||||||||||||||
|
Comment on lines
191
to
194
|
||||||||||||||||||||||||||||
| with self.assertRaises(ValueError): | ||||||||||||||||||||||||||||
| get_transport(ssh=FakeSSH()) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def test_get_job_from_remote(self): | ||||||||||||||||||||||||||||
| path = os.path.dirname(os.path.abspath(__file__)) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex")) | ||||||||||||||||||||||||||||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||||||||||||||||||||||||||||
| remote._adapter._ssh_remote_path = path | ||||||||||||||||||||||||||||
| remote._adapter._ssh_local_path = path | ||||||||||||||||||||||||||||
| remote._adapter._ssh_delete_file_on_remote = True | ||||||||||||||||||||||||||||
|
Comment on lines
199
to
203
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,21 +19,21 @@ | |
| class TestRemoteQueueAdapterAuth(unittest.TestCase): | ||
| def test_password_auth(self): | ||
| path = os.path.dirname(os.path.abspath(__file__)) | ||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex")) | ||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||
| remote._adapter._ssh_ask_for_password = False | ||
| remote._adapter._ssh_key = None | ||
| self.assertIsNotNone(remote._adapter._open_ssh_connection()) | ||
|
Comment on lines
20
to
25
|
||
|
|
||
| def test_key_auth(self): | ||
| path = os.path.dirname(os.path.abspath(__file__)) | ||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex")) | ||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||
| remote._adapter._ssh_password = None | ||
| with self.assertRaises(ValueError): | ||
| remote._adapter._open_ssh_connection() | ||
|
|
||
| def test_two_factor_auth(self): | ||
| path = os.path.dirname(os.path.abspath(__file__)) | ||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex")) | ||
| remote = QueueAdapter(directory=os.path.join(path, "../../static/remote_rebex_hosts")) | ||
| remote._adapter._ssh_two_factor_authentication = True | ||
| with self.assertRaises(paramiko.ssh_exception.AuthenticationException): | ||
| remote._adapter._open_ssh_connection() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
known_hostsis treated as present solely based on the key existing inconfig. If a user setsknown_hosts: ""(or it gets parsed as an empty string),abspath(expanduser(""))becomes the current working directory andload_host_keys()will later try to read a directory path. Consider usingknown_hosts = config.get("known_hosts")and only setting_ssh_known_hostswhen it is a non-empty path (otherwise treat it as unset).