Skip to content

Commit 1b1cfa5

Browse files
authored
[ISD-3695] Fix: False SSH connection errors (#572)
1 parent b277a3a commit 1b1cfa5

File tree

3 files changed

+100
-33
lines changed

3 files changed

+100
-33
lines changed

docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
### 2025-06-17
4+
5+
- Fix bug where SSH connection error always appears in the logs.
6+
37
### 2025-06-16
48

59
- Revert copytruncate logrotate method for reactive processes, as copytruncate keeps log files on disks and does not remove them, and each process is writing to a new file leading to a huge and increasing amount

github-runner-manager/src/github_runner_manager/openstack_cloud/openstack_cloud.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ def get_ssh_connection(self, instance: OpenstackInstance) -> Iterator[SSHConnect
366366
continue
367367
if _TEST_STRING in result.stdout:
368368
yield connection
369+
break
369370
except NoValidConnectionsError as exc:
370371
logger.warning(
371372
"NoValidConnectionsError. Unable to SSH into %s with address %s. Error: %s",
@@ -384,10 +385,11 @@ def get_ssh_connection(self, instance: OpenstackInstance) -> Iterator[SSHConnect
384385
continue
385386
finally:
386387
connection.close()
387-
raise SSHError(
388-
f"No connectable SSH addresses found, server: {instance.instance_id.name}, "
389-
f"addresses: {instance.addresses}"
390-
)
388+
else:
389+
raise SSHError(
390+
f"No connectable SSH addresses found, server: {instance.instance_id.name}, "
391+
f"addresses: {instance.addresses}"
392+
)
391393

392394
@_catch_openstack_errors
393395
def get_instances(self) -> tuple[OpenstackInstance, ...]:

github-runner-manager/tests/unit/openstack_cloud/test_openstack_cloud.py

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
from openstack.network.v2.security_group import SecurityGroup as OpenstackSecurityGroup
1818
from openstack.network.v2.security_group_rule import SecurityGroupRule
1919

20-
from github_runner_manager.errors import OpenStackError
20+
from github_runner_manager.errors import OpenStackError, SSHError
2121
from github_runner_manager.openstack_cloud.openstack_cloud import (
2222
_MIN_KEYPAIR_AGE_IN_SECONDS_BEFORE_DELETION,
23+
_TEST_STRING,
2324
DEFAULT_SECURITY_RULES,
2425
OpenstackCloud,
2526
OpenStackCredentials,
@@ -32,6 +33,24 @@
3233
logger = logging.getLogger(__name__)
3334

3435

36+
@pytest.fixture(name="openstack_cloud", scope="function")
37+
def openstack_cloud_fixture(monkeypatch):
38+
# Mock expanduser as this is used in OpenstackCloud constructor
39+
monkeypatch.setattr(
40+
"github_runner_manager.openstack_cloud.openstack_cloud.Path.expanduser", MagicMock()
41+
)
42+
creds = OpenStackCredentials(
43+
username=FAKE_ARG,
44+
password=FAKE_ARG,
45+
project_name=FAKE_ARG,
46+
user_domain_name=FAKE_ARG,
47+
project_domain_name=FAKE_ARG,
48+
auth_url=FAKE_ARG,
49+
region_name=FAKE_ARG,
50+
)
51+
return OpenstackCloud(creds, FAKE_PREFIX, FAKE_ARG)
52+
53+
3554
@pytest.mark.parametrize(
3655
"public_method, args",
3756
[
@@ -51,28 +70,21 @@
5170
],
5271
)
5372
def test_raises_openstack_error(
54-
public_method: str, args: dict[Any, Any], monkeypatch: pytest.MonkeyPatch
73+
openstack_cloud: OpenstackCloud,
74+
public_method: str,
75+
args: dict[Any, Any],
76+
monkeypatch: pytest.MonkeyPatch,
5577
):
5678
"""
5779
arrange: Mock OpenstackCloud and openstack.connect to raise an Openstack api exception.
5880
act: Call a public method which connects to Openstack.
5981
assert: OpenStackError is raised.
6082
"""
61-
creds = OpenStackCredentials(
62-
username=FAKE_ARG,
63-
password=FAKE_ARG,
64-
project_name=FAKE_ARG,
65-
user_domain_name=FAKE_ARG,
66-
project_domain_name=FAKE_ARG,
67-
auth_url=FAKE_ARG,
68-
region_name=FAKE_ARG,
69-
)
7083
# Mock expanduser as this is used in OpenstackCloud constructor
7184
monkeypatch.setattr(
7285
"github_runner_manager.openstack_cloud.openstack_cloud.Path.expanduser", MagicMock()
7386
)
7487

75-
cloud = OpenstackCloud(creds, FAKE_ARG, FAKE_ARG)
7688
openstack_connect_mock = MagicMock(spec=openstack.connect)
7789

7890
excs = (openstack.exceptions.SDKException, keystoneauth1.exceptions.ClientException)
@@ -83,7 +95,7 @@ def test_raises_openstack_error(
8395
openstack_connect_mock,
8496
)
8597
with pytest.raises(OpenStackError) as innerexc:
86-
getattr(cloud, public_method)(**args)
98+
getattr(openstack_cloud, public_method)(**args)
8799
assert "Failed OpenStack API call" in str(innerexc.value)
88100

89101

@@ -142,7 +154,9 @@ def test_missing_security_rules(security_rules, extra_ports, expected_missing_ru
142154
assert missing == expected_missing_rules
143155

144156

145-
def test_keypair_cleanup_freshly_created_keypairs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
157+
def test_keypair_cleanup_freshly_created_keypairs(
158+
openstack_cloud: OpenstackCloud, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
159+
):
146160
"""
147161
arrange: Keypairs with different creation time.
148162
act: Call cleanup.
@@ -154,18 +168,8 @@ def test_keypair_cleanup_freshly_created_keypairs(monkeypatch: pytest.MonkeyPatc
154168
"github_runner_manager.openstack_cloud.openstack_cloud.Path.expanduser", MagicMock()
155169
)
156170

157-
creds = OpenStackCredentials(
158-
username=FAKE_ARG,
159-
password=FAKE_ARG,
160-
project_name=FAKE_ARG,
161-
user_domain_name=FAKE_ARG,
162-
project_domain_name=FAKE_ARG,
163-
auth_url=FAKE_ARG,
164-
region_name=FAKE_ARG,
165-
)
166-
cloud = OpenstackCloud(creds, FAKE_PREFIX, FAKE_ARG)
167-
cloud._ssh_key_dir = tmp_path / "ssh_key_dir"
168-
cloud._ssh_key_dir.mkdir()
171+
openstack_cloud._ssh_key_dir = tmp_path / "ssh_key_dir"
172+
openstack_cloud._ssh_key_dir.mkdir()
169173

170174
openstack_connect_mock = MagicMock(spec=openstack.connect)
171175
monkeypatch.setattr(
@@ -176,7 +180,7 @@ def test_keypair_cleanup_freshly_created_keypairs(monkeypatch: pytest.MonkeyPatc
176180
now = _mock_datetime_now(monkeypatch)
177181
keypairs_older_or_same_min_age = (
178182
(
179-
cloud._ssh_key_dir / f"{FAKE_PREFIX}-old-{i}.key",
183+
openstack_cloud._ssh_key_dir / f"{FAKE_PREFIX}-old-{i}.key",
180184
now - datetime.timedelta(seconds=seconds),
181185
)
182186
for i, seconds in enumerate(
@@ -189,7 +193,7 @@ def test_keypair_cleanup_freshly_created_keypairs(monkeypatch: pytest.MonkeyPatc
189193
)
190194
keypairs_younger_than_min_age = (
191195
(
192-
cloud._ssh_key_dir / f"{FAKE_PREFIX}-new-{i}.key",
196+
openstack_cloud._ssh_key_dir / f"{FAKE_PREFIX}-new-{i}.key",
193197
now - datetime.timedelta(seconds=seconds),
194198
)
195199
for i, seconds in enumerate((1, _MIN_KEYPAIR_AGE_IN_SECONDS_BEFORE_DELETION - 1))
@@ -214,7 +218,7 @@ def test_keypair_cleanup_freshly_created_keypairs(monkeypatch: pytest.MonkeyPatc
214218
openstack_connect_mock.return_value = openstack_connection_mock
215219

216220
# act #
217-
cloud.cleanup()
221+
openstack_cloud.cleanup()
218222

219223
# assert #
220224
# Check if only the old keypairs are deleted
@@ -241,3 +245,60 @@ def _mock_datetime_now(monkeypatch):
241245
"github_runner_manager.openstack_cloud.openstack_cloud.datetime", datetime_mock
242246
)
243247
return now
248+
249+
250+
def test_get_ssh_connection_success(openstack_cloud, monkeypatch):
251+
"""
252+
arrange: Setup SSH connection to succeed.
253+
act: Get SSH connection.
254+
assert: No SSHError raised. No SSH connection errors.
255+
"""
256+
mock_result = MagicMock(ok=True, stdout=_TEST_STRING)
257+
mock_connection = MagicMock(run=MagicMock(return_value=mock_result))
258+
259+
def mock_ssh_connection(*_args, **_kwargs) -> MagicMock:
260+
"""Mock get_ssh_connection function.
261+
262+
Returns:
263+
The mocked connection.
264+
"""
265+
return mock_connection
266+
267+
monkeypatch.setattr(
268+
"github_runner_manager.openstack_cloud.openstack_cloud.SSHConnection", mock_ssh_connection
269+
)
270+
271+
mock_instance = MagicMock()
272+
mock_instance.addresses = ["mock_ip"]
273+
with openstack_cloud.get_ssh_connection(mock_instance) as conn:
274+
assert conn == mock_connection
275+
276+
277+
def test_get_ssh_connection_failure(openstack_cloud, monkeypatch):
278+
"""
279+
arrange: Setup SSH connection to fail.
280+
act: Get SSH connection.
281+
assert: Error raised with no connectable SSH address found.
282+
"""
283+
mock_result = MagicMock(ok=False, stdout=_TEST_STRING)
284+
mock_connection = MagicMock(run=MagicMock(return_value=mock_result))
285+
286+
def mock_ssh_connection(*_args, **_kwargs) -> MagicMock:
287+
"""Mock get_ssh_connection function.
288+
289+
Returns:
290+
The mocked connection.
291+
"""
292+
return mock_connection
293+
294+
monkeypatch.setattr(
295+
"github_runner_manager.openstack_cloud.openstack_cloud.SSHConnection", mock_ssh_connection
296+
)
297+
298+
mock_instance = MagicMock()
299+
mock_instance.addresses = ["mock_ip"]
300+
with pytest.raises(SSHError) as err:
301+
with openstack_cloud.get_ssh_connection(mock_instance):
302+
pass
303+
304+
assert "No connectable SSH addresses found" in str(err.value)

0 commit comments

Comments
 (0)