Skip to content

Commit 98e34a3

Browse files
lmicciniclaude
andcommitted
Retry Redfish power-on after 400 Bad Request when server is OFF
Some hardware (e.g. Fujitsu PRIMEQUEST 4400E) returns 400 Bad Request when an On operation is sent shortly after ForceOff, because the system needs additional time before it can accept a power-on command even though PowerState is already Off. This is compliant with the Redfish spec where PowerState and action permissibility are separate concepts. Previously, a 400 with power state OFF was treated as a terminal failure. Now, for On/ForceOn actions, the code retries with backoff (same as 5xx errors), while ForceOff actions with power state OFF still succeed immediately as before. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b686cc0 commit 98e34a3

2 files changed

Lines changed: 106 additions & 3 deletions

File tree

templates/instanceha/bin/instanceha.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,9 +1870,27 @@ def _redfish_reset(url, user, passwd, timeout, action, config_mgr):
18701870
# Check if server is already powered off
18711871
power_state = _get_power_state('redfish', url=url, user=user, passwd=passwd, timeout=timeout, config_mgr=config_mgr)
18721872
if power_state == 'OFF':
1873-
logging.debug("Redfish reset successful: %s on %s (already off)", action, safe_url)
1874-
logging.info("Redfish reset successful: %s (already off)", action)
1875-
return True
1873+
if action in ['On', 'ForceOn']:
1874+
# Server is OFF but not yet ready to accept power-on
1875+
# (e.g. PRIMEQUEST 4400E needs time after ForceOff before
1876+
# accepting On). Retry with backoff.
1877+
if attempt < MAX_FENCING_RETRIES - 1:
1878+
logging.warning(
1879+
"Redfish reset: %s rejected with %d while server is OFF "
1880+
"(attempt %d/%d), server may not be ready yet, retrying...",
1881+
action, response.status_code, attempt + 1, MAX_FENCING_RETRIES)
1882+
time.sleep(FENCING_RETRY_DELAY_SECONDS * (attempt + 1))
1883+
continue
1884+
else:
1885+
logging.error(
1886+
"Redfish reset failed: %s rejected with %d while server is OFF "
1887+
"after %d attempts for %s",
1888+
action, response.status_code, MAX_FENCING_RETRIES, safe_url)
1889+
return False
1890+
else:
1891+
logging.debug("Redfish reset successful: %s on %s (already off)", action, safe_url)
1892+
logging.info("Redfish reset successful: %s (already off)", action)
1893+
return True
18761894
else:
18771895
logging.error("Redfish reset failed: %s conflict but not OFF (status: %s) for %s", response.status_code, power_state, safe_url)
18781896
return False

test/instanceha/test_fencing_agents.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,91 @@ def test_redfish_reset_400_bad_request_not_off(self, mock_get, mock_post):
201201
mock_post.assert_called_once()
202202
mock_get.assert_called_once()
203203

204+
@patch('instanceha.requests.post')
205+
@patch('instanceha.requests.get')
206+
@patch('instanceha.time.sleep')
207+
def test_redfish_reset_400_on_action_retries_when_off(self, mock_sleep, mock_get, mock_post):
208+
"""Test Redfish reset retries On action with 400 when server is OFF.
209+
210+
Some hardware (e.g. PRIMEQUEST 4400E) returns 400 Bad Request when
211+
an On operation is sent too soon after ForceOff, because the system
212+
is not yet ready to accept the power-on command even though
213+
PowerState is already Off. The retry logic should handle this.
214+
"""
215+
mock_config_manager = Mock()
216+
mock_config_manager.get_requests_ssl_config.return_value = False
217+
218+
# First two POST attempts return 400, third succeeds with 200
219+
mock_post.side_effect = [
220+
Mock(status_code=400),
221+
Mock(status_code=400),
222+
Mock(status_code=200),
223+
]
224+
225+
# GET returns OFF for both 400 responses (server is off but not ready)
226+
mock_get_response = Mock()
227+
mock_get_response.status_code = 200
228+
mock_get_response.json.return_value = {'PowerState': 'Off'}
229+
mock_get.return_value = mock_get_response
230+
231+
result = instanceha._redfish_reset(
232+
'http://test-server/redfish/v1/Systems/1', 'user', 'pass', 30,
233+
'On', mock_config_manager)
234+
235+
self.assertTrue(result)
236+
self.assertEqual(mock_post.call_count, 3)
237+
self.assertEqual(mock_get.call_count, 2) # Called on each 400
238+
self.assertEqual(mock_sleep.call_count, 2) # Sleep between retries
239+
240+
@patch('instanceha.requests.post')
241+
@patch('instanceha.requests.get')
242+
@patch('instanceha.time.sleep')
243+
def test_redfish_reset_400_on_action_retry_exhausted(self, mock_sleep, mock_get, mock_post):
244+
"""Test Redfish reset fails after max retries on 400 for On action."""
245+
mock_config_manager = Mock()
246+
mock_config_manager.get_requests_ssl_config.return_value = False
247+
248+
# All POST attempts return 400
249+
mock_post.return_value = Mock(status_code=400)
250+
251+
# GET always returns OFF (server off but never ready)
252+
mock_get_response = Mock()
253+
mock_get_response.status_code = 200
254+
mock_get_response.json.return_value = {'PowerState': 'Off'}
255+
mock_get.return_value = mock_get_response
256+
257+
result = instanceha._redfish_reset(
258+
'http://test-server/redfish/v1/Systems/1', 'user', 'pass', 30,
259+
'On', mock_config_manager)
260+
261+
self.assertFalse(result)
262+
self.assertEqual(mock_post.call_count, 3) # MAX_FENCING_RETRIES
263+
self.assertEqual(mock_get.call_count, 3)
264+
self.assertEqual(mock_sleep.call_count, 2) # No sleep after last attempt
265+
266+
@patch('instanceha.requests.post')
267+
@patch('instanceha.requests.get')
268+
def test_redfish_reset_400_forceoff_already_off(self, mock_get, mock_post):
269+
"""Test Redfish reset with 400 for ForceOff when already off succeeds immediately."""
270+
mock_config_manager = Mock()
271+
mock_config_manager.get_requests_ssl_config.return_value = False
272+
273+
mock_post.return_value = Mock(status_code=400)
274+
275+
mock_get_response = Mock()
276+
mock_get_response.status_code = 200
277+
mock_get_response.json.return_value = {'PowerState': 'Off'}
278+
mock_get.return_value = mock_get_response
279+
280+
result = instanceha._redfish_reset(
281+
'http://test-server/redfish/v1/Systems/1', 'user', 'pass', 30,
282+
'ForceOff', mock_config_manager)
283+
284+
# ForceOff with 400 and power state OFF should succeed immediately (no retry)
285+
self.assertTrue(result)
286+
mock_post.assert_called_once()
287+
mock_get.assert_called_once()
288+
204289
@patch('instanceha.requests.post')
205290
def test_redfish_reset_success(self, mock_post):
206291
"""Test successful Redfish reset."""

0 commit comments

Comments
 (0)