Skip to content

Commit e3c9401

Browse files
Merge pull request #553 from lmiccini/OSPRH-28415
Retry Redfish power-on after 400 Bad Request when server is OFF
2 parents b686cc0 + 98e34a3 commit e3c9401

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)