Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions ironic/drivers/modules/redfish/boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,15 @@ def _insert_vmedia_in_resource(task, resource, boot_url, boot_device,
try:
v_media.insert_media(boot_url, inserted=True,
write_protected=True)
# NOTE(janders): On Cisco C845A M8 (and potentially other OpenBMC
# systems), some virtual media slots only support local/KVM access
# via WebSocket/NBD and do not have the InsertMedia action.
# Catch MissingActionError and try the next available device.
except sushy.exceptions.MissingActionError:
LOG.info("Virtual media device %(slot)s on node %(node)s does "
"not support InsertMedia action, skipping.",
{'slot': v_media.identity, 'node': task.node.uuid})
continue
# NOTE(janders): On Cisco UCSB and UCSX blades there are several
# vMedia devices. Some of those are only meant for internal use
# by CIMC vKVM - attempts to InsertMedia into those will result
Expand All @@ -375,6 +384,24 @@ def _insert_vmedia_in_resource(task, resource, boot_url, boot_device,
except sushy.exceptions.ServerSideError as e:
e.node_uuid = task.node.uuid
raise
# NOTE(janders): On Cisco C845A M8 (and potentially other systems),
# attempting to use a virtual media slot that doesn't support remote
# media insertion may result in HTTP 405 (Method Not Allowed).
# Catch this and try the next available device.
# This must come after ServerSideError since it is a subclass of
# HTTPError.
except sushy.exceptions.HTTPError as exc:
if exc.status_code == 405:
err_msg = ("Inserting virtual media into %(boot_device)s "
"failed for node %(node)s, moving to next "
"virtual media device, if available. "
"%(exc)s" %
{'node': task.node.uuid, 'exc': exc,
'boot_device': boot_device})
err_msgs.append(err_msg)
LOG.warning(err_msg)
continue
raise

LOG.info("Inserted boot media %(boot_url)s into "
"%(boot_device)s for node "
Expand Down
77 changes: 77 additions & 0 deletions ironic/tests/unit/drivers/modules/redfish/test_boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,83 @@ def clear_and_raise(*args, **kwargs):

self.assertEqual(mock_vmedia_dvd_2.insert_media.call_count, 1)

@mock.patch('time.sleep', lambda *args, **kwargs: None)
@mock.patch.object(redfish_boot, '_has_vmedia_via_systems', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test__insert_vmedia_skip_no_action(self, mock_sys, mock_vmd_sys):
"""Test that vMedia slots without InsertMedia action are skipped."""
mock_vmd_sys.return_value = False
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
# First slot has no InsertMedia action (like Cisco C845A M8 Slot_0)
mock_vmedia_no_action = mock.MagicMock(
inserted=False,
identity='Slot_0',
media_types=[sushy.VIRTUAL_MEDIA_CD])
mock_vmedia_no_action.insert_media.side_effect = (
sushy.exceptions.MissingActionError(
action='#VirtualMedia.InsertMedia',
resource=mock_vmedia_no_action.path))

# Second slot has InsertMedia action (like Cisco C845A M8 Slot_2)
mock_vmedia_with_action = mock.MagicMock(
inserted=False,
identity='Slot_2',
media_types=[sushy.VIRTUAL_MEDIA_CD])

mock_manager = mock.MagicMock()
mock_manager.virtual_media.get_members.return_value = [
mock_vmedia_no_action, mock_vmedia_with_action]

redfish_boot._insert_vmedia(
task, [mock_manager], 'img-url', sushy.VIRTUAL_MEDIA_CD)

# First slot should raise MissingActionError and be skipped
self.assertTrue(mock_vmedia_no_action.insert_media.called)
self.assertTrue(mock_vmedia_with_action.insert_media.called)

@mock.patch('time.sleep', lambda *args, **kwargs: None)
@mock.patch.object(redfish_boot, '_has_vmedia_via_systems', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test__insert_vmedia_retry_on_http_405(self, mock_sys, mock_vmd_sys):
"""Test that HTTP 405 errors trigger retry with next slot."""
mock_vmd_sys.return_value = False
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
# First slot raises HTTP 405
mock_vmedia_405 = mock.MagicMock(
inserted=False,
identity='Slot_1',
media_types=[sushy.VIRTUAL_MEDIA_CD])
mock_vmedia_405._actions = mock.MagicMock()
mock_vmedia_405._actions.insert_media = mock.MagicMock()

mock_response = mock.MagicMock()
mock_response.status_code = 405
http_405_error = sushy.exceptions.HTTPError(
"PATCH", 'img-url', mock_response)
http_405_error.status_code = 405
mock_vmedia_405.insert_media.side_effect = http_405_error

# Second slot succeeds
mock_vmedia_success = mock.MagicMock(
inserted=False,
identity='Slot_2',
media_types=[sushy.VIRTUAL_MEDIA_CD])
mock_vmedia_success._actions = mock.MagicMock()
mock_vmedia_success._actions.insert_media = mock.MagicMock()

mock_manager = mock.MagicMock()
mock_manager.virtual_media.get_members.return_value = [
mock_vmedia_405, mock_vmedia_success]

redfish_boot._insert_vmedia(
task, [mock_manager], 'img-url', sushy.VIRTUAL_MEDIA_CD)

# First slot should fail, second slot should succeed
self.assertTrue(mock_vmedia_405.insert_media.called)
self.assertTrue(mock_vmedia_success.insert_media.called)

@mock.patch.object(redfish_boot, '_has_vmedia_via_systems', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test__insert_vmedia_already_inserted(self, mock_sys, mock_vmd_sys):
Expand Down
10 changes: 10 additions & 0 deletions releasenotes/notes/cisco-c845a-vmedia-fix-b98a4fadd58a106d.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
fixes:
- |
Fixes virtual media insertion failures on Cisco C845A M8 (and potentially
other OpenBMC-based systems) where some virtual media slots only support
local/KVM access via WebSocket/NBD and do not expose the ``InsertMedia``
Redfish action. Ironic now checks for the presence of the ``InsertMedia``
action before attempting to use a virtual media slot, and also catches
HTTP 405 (Method Not Allowed) errors to gracefully fall back to the next
available slot.