From e2d6dbbb9230ece4baa32af5dbebf0cbf8446d79 Mon Sep 17 00:00:00 2001 From: Jacob Anders Date: Mon, 23 Feb 2026 20:11:52 +1000 Subject: [PATCH] Fix vMedia insertion on Cisco C845A M8 and similar OpenBMC systems Some virtual media slots on Cisco C845A M8 (and potentially other OpenBMC-based systems) only support local/KVM access via WebSocket/NBD and do not expose the InsertMedia Redfish action. Attempting to use these slots results in HTTP 405 (Method Not Allowed). This patch adds a pre-check to skip slots without the InsertMedia action, and catches HTTP 405 errors as a fallback to try the next available slot. Change-Id: I6242c62f691bf642708c9ab147522cee88edd3a7 Signed-off-by: Jacob Anders Assisted-By: Claude Code Opus 4.6 (cherry picked from commit 8c95a7b50771024f7dca05c4e473a265b9b1ecf3) (cherry picked from commit 3be7de98b1196ca0837f3ceca25c3ee60f5bfb94) (cherry picked from commit bf4ff2ecaf1f7e7860b0e233270b86acd95fb4b6) --- ironic/drivers/modules/redfish/boot.py | 27 +++++++ .../unit/drivers/modules/redfish/test_boot.py | 77 +++++++++++++++++++ ...sco-c845a-vmedia-fix-b98a4fadd58a106d.yaml | 10 +++ 3 files changed, 114 insertions(+) create mode 100644 releasenotes/notes/cisco-c845a-vmedia-fix-b98a4fadd58a106d.yaml diff --git a/ironic/drivers/modules/redfish/boot.py b/ironic/drivers/modules/redfish/boot.py index 1a4a0c5372..3971b765c7 100644 --- a/ironic/drivers/modules/redfish/boot.py +++ b/ironic/drivers/modules/redfish/boot.py @@ -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 @@ -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 " diff --git a/ironic/tests/unit/drivers/modules/redfish/test_boot.py b/ironic/tests/unit/drivers/modules/redfish/test_boot.py index c32d7ccbf1..784fe5db65 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_boot.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_boot.py @@ -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): diff --git a/releasenotes/notes/cisco-c845a-vmedia-fix-b98a4fadd58a106d.yaml b/releasenotes/notes/cisco-c845a-vmedia-fix-b98a4fadd58a106d.yaml new file mode 100644 index 0000000000..117899f22f --- /dev/null +++ b/releasenotes/notes/cisco-c845a-vmedia-fix-b98a4fadd58a106d.yaml @@ -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.