Skip to content

Commit 3067c82

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Fix RuntimeError when stopping heartbeater in rescue mode"
2 parents 01cbcd6 + bae591a commit 3067c82

File tree

3 files changed

+76
-3
lines changed

3 files changed

+76
-3
lines changed

ironic_python_agent/agent.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,10 @@ def stop(self):
177177
"""Stop the heartbeat thread."""
178178
LOG.info('stopping heartbeater')
179179
self.stop_event.set()
180-
return self.join()
180+
# Only join if the thread was actually started
181+
if self.is_alive():
182+
return self.join()
183+
return None
181184

182185

183186
class IronicPythonAgent(base.ExecuteCommandMixin):
@@ -621,13 +624,15 @@ def run(self):
621624
# rescue operations.
622625
LOG.info('Found rescue state marker file, locking down IPA '
623626
'in disabled mode')
624-
self.heartbeater.stop()
627+
if hasattr(self, 'heartbeater') and self.heartbeater.is_alive():
628+
self.heartbeater.stop()
625629
self.serve_api = False
626630
while True:
627631
time.sleep(100)
628632

629633
if not self.standalone and self.api_urls:
630-
self.heartbeater.stop()
634+
if hasattr(self, 'heartbeater') and self.heartbeater.is_alive():
635+
self.heartbeater.stop()
631636

632637
if self.lockdown:
633638
self._lockdown_system()

ironic_python_agent/tests/unit/test_agent.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,25 @@ def test__heartbeat_expected(self, mock_time):
184184
mock_time.return_value = 110
185185
self.assertTrue(self.heartbeater._heartbeat_expected())
186186

187+
def test_stop_not_started(self):
188+
"""Test stop() when thread was never started."""
189+
# Thread is not alive, should not call join()
190+
self.assertFalse(self.heartbeater.is_alive())
191+
result = self.heartbeater.stop()
192+
self.assertIsNone(result)
193+
self.assertTrue(self.heartbeater.stop_event.set.called)
194+
195+
@mock.patch.object(agent.IronicPythonAgentHeartbeater, 'join',
196+
autospec=True)
197+
def test_stop_when_alive(self, mock_join):
198+
"""Test stop() when thread is alive."""
199+
# Mock the thread as alive
200+
with mock.patch.object(self.heartbeater, 'is_alive',
201+
autospec=True, return_value=True):
202+
self.heartbeater.stop()
203+
mock_join.assert_called_once_with(self.heartbeater)
204+
self.assertTrue(self.heartbeater.stop_event.set.called)
205+
187206

188207
@mock.patch.object(hardware, '_md_scan_and_assemble', lambda: None)
189208
@mock.patch.object(hardware, '_check_for_iscsi', lambda: None)
@@ -800,6 +819,49 @@ def set_serve_api(*args, **kwargs):
800819
mock.call('wait_for_disks')],
801820
mock_dispatch.call_args_list)
802821

822+
@mock.patch(
823+
'ironic_python_agent.hardware_managers.cna._detect_cna_card',
824+
mock.Mock())
825+
@mock.patch('os.path.exists', autospec=True)
826+
@mock.patch.object(hardware, 'get_managers', autospec=True)
827+
@mock.patch.object(time, 'sleep', autospec=True)
828+
@mock.patch.object(agent.IronicPythonAgent, '_wait_for_interface',
829+
autospec=True)
830+
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
831+
def test_run_rescue_mode_heartbeater_not_started(
832+
self, mock_dispatch, mock_wait, mock_sleep, mock_get_managers,
833+
mock_exists):
834+
"""Test rescue mode doesn't fail when heartbeater not started."""
835+
CONF.set_override('inspection_callback_url', '')
836+
837+
# Mock rescue mode marker file exists
838+
mock_exists.return_value = True
839+
840+
self.agent.heartbeater = mock.Mock()
841+
self.agent.heartbeater.is_alive.return_value = False
842+
self.agent.api_client.lookup_node = mock.Mock()
843+
self.agent.api_client.lookup_node.return_value = {
844+
'node': {
845+
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
846+
},
847+
'config': {
848+
'heartbeat_timeout': 300
849+
}
850+
}
851+
852+
# Setup to exit the infinite loop after first iteration
853+
mock_sleep.side_effect = [None, KeyboardInterrupt()]
854+
855+
try:
856+
self.agent.run()
857+
except KeyboardInterrupt:
858+
pass
859+
860+
# Heartbeater should not be started or stopped in rescue mode
861+
self.agent.heartbeater.start.assert_not_called()
862+
self.agent.heartbeater.stop.assert_not_called()
863+
self.assertFalse(self.agent.serve_api)
864+
803865
def test_async_command_success(self):
804866
result = base.AsyncCommandResult('foo_command', {'fail': False},
805867
foo_execute)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
Fixes RuntimeError when entering rescue mode by checking if the
5+
heartbeater thread is alive before attempting to stop it.
6+

0 commit comments

Comments
 (0)