Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
fixes:
- |
Improve Redfish VirtualMedia ``InsertMedia`` error handling on BMCs that
require credentials but return unstructured errors (e.g., responses that
omit ``error.code`` and report ``ActionParameterMissing`` only via
``@Message.ExtendedInfo`` or free-text messages). Sushy now detects these
cases and retries with ``UserName``/``Password`` parameters, allowing ISO
mounting to proceed. This also preserves compatibility with legacy
``error.code == *GeneralError`` responses that mention missing parameters.
33 changes: 31 additions & 2 deletions sushy/resources/manager/virtual_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,37 @@ def is_credentials_required(self, error=None):

Try to determine if it happened due to missing Credentials
"""
if (error.code.endswith('GeneralError')
and 'UserName' in error.detail):

# Accept broader variants seen on NVIDIA DGX where "code" may be absent
try:
body = error.response.json()
except Exception:
body = {}

# Structured path: @Message.ExtendedInfo entries
ext = (body.get('error', {}) or {}).get('@Message.ExtendedInfo',
[]) or []
for m in ext:
mid = (m.get('MessageId') or '')
msg = (m.get('Message') or '')
if mid.endswith('ActionParameterMissing') and (
'UserName' in msg or 'Password' in msg):
return True

# Legacy/structured: error.code + message mentions UserName/Password
err_obj = (body.get('error') or {})
code = (err_obj.get('code') or '')
msg = (err_obj.get('message') or '')
if code.endswith('GeneralError') and (
'UserName' in msg or 'Password' in msg
or 'parameter missing' in msg):
return True

# Text fallback: common strings observed in the field
raw_msg = ((body.get('error') or {}).get('message') or '') + ' ' + (
error.detail or '')
if ('requires the parameter UserName' in raw_msg
or 'requires the parameter Password' in raw_msg):
return True
return False

Expand Down
91 changes: 90 additions & 1 deletion sushy/tests/unit/resources/manager/test_virtual_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from http import client as http_client
import json
import types
from unittest import mock

import sushy
Expand All @@ -24,6 +24,18 @@
from sushy.tests.unit import base


class _FakeHTTPError(Exception):
"""Mimics the object passed to is_credentials_required()."""

def __init__(self, status=400, json_body=None, detail=''):
super().__init__(detail)
self.detail = detail
self.response = types.SimpleNamespace(
status_code=status,
json=lambda: json_body if json_body is not None else {}
)


class VirtualMediaTestCase(base.TestCase):

def setUp(self):
Expand Down Expand Up @@ -356,3 +368,80 @@ def test_certificate_collection(self, mock_cert_coll):
redfish_version='1.0.2',
registries=self.sys_virtual_media.registries,
root=self.sys_virtual_media.root)

def test_detects_extended_info_username(self):
body = {
"error": {
"@Message.ExtendedInfo": [{
"MessageId": "Base.1.12.ActionParameterMissing",
"Message": "The action requires the parameter UserName to "
"be present."
}]
}
}
err = _FakeHTTPError(json_body=body)
self.assertTrue(self.sys_virtual_media.is_credentials_required(err))

def test_detects_extended_info_password(self):
body = {
"error": {
"@Message.ExtendedInfo": [{
"MessageId": "Base.1.15.ActionParameterMissing",
"Message": "The action requires the parameter Password to "
"be present."
}]
}
}
err = _FakeHTTPError(json_body=body)
self.assertTrue(self.sys_virtual_media.is_credentials_required(err))

def test_detects_plain_message_when_code_absent(self):
body = {"error": {
"message": "InsertMedia requires the parameter UserName"}}
err = _FakeHTTPError(json_body=body, detail="")
self.assertTrue(self.sys_virtual_media.is_credentials_required(err))

def test_detects_detail_fallback(self):
err = _FakeHTTPError(json_body={},
detail="requires the parameter Password")
self.assertTrue(self.sys_virtual_media.is_credentials_required(err))

def test_preserves_legacy_code_based_detection(self):
body = {
"error": {
"code": "Base.1.10.GeneralError",
"message": "Action parameter missing: UserName"
}
}
err = _FakeHTTPError(json_body=body)
self.assertTrue(self.sys_virtual_media.is_credentials_required(err))

def test_no_trigger_on_unrelated_parameter(self):
body = {
"error": {
"@Message.ExtendedInfo": [{
"MessageId": "Base.1.12.ActionParameterMissing",
"Message": "The action requires the parameter Timeout to "
"be present."
}]
}
}
err = _FakeHTTPError(json_body=body)
self.assertFalse(self.sys_virtual_media.is_credentials_required(err))

def test_no_trigger_on_unrelated_error(self):
body = {
"error": {
"@Message.ExtendedInfo": [{
"MessageId": "Base.1.12.ResourceMissingAtURI",
"Message": "Resource missing"
}]
}
}
err = _FakeHTTPError(json_body=body)
self.assertFalse(self.sys_virtual_media.is_credentials_required(err))

def test_no_trigger_when_no_clues(self):
err = _FakeHTTPError(json_body={"error": {"message": "Bad Request"}},
detail="")
self.assertFalse(self.sys_virtual_media.is_credentials_required(err))