Expose System sub-resources based on driver support

Only advertise BIOS, Processors, and SimpleStorage when the driver
supports them, preventing non-functional endpoints.

Driver support matrix:

| Driver    | BIOS | Processors | SimpleStorage |
|-----------|------|------------|---------------|
| libvirt   | ✓    | ✓          | ✓             |
| nova      | ✗    | ✗          | ✗             |
| ironic    | ✗    | ✗          | ✗             |
| fake      | ✗    | ✗          | ✗             |

Assisted-By: Claude Code/claude-4.5-sonnet
Change-Id: I12da36642a92ff6e66c23987c2a7d0f002f0f435
Signed-off-by: Harald Jensås <hjensas@redhat.com>
This commit is contained in:
Harald Jensås
2025-10-27 22:48:20 +01:00
parent b900c41b74
commit 0e08c93114
4 changed files with 207 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
---
fixes:
- |
System sub-resources (``BIOS``, ``Processors``, and ``SimpleStorage``) are
now conditionally exposed based on driver support. Only the libvirt driver
implements these features, so they will only appear in the System resource
response when using the libvirt backend. This prevents exposing non-
functional endpoints for drivers that don't support these capabilities.
When these fields are absent, Redfish clients like Ironic will receive a
``MissingAttributeError`` and handle it gracefully.

View File

@@ -468,6 +468,10 @@ def system_resource(identity):
power_state=app.systems.get_power_state(identity),
total_memory_gb=try_get(app.systems.get_total_memory),
bios_version=bios_version,
bios_supported=try_get(app.systems.get_bios),
processors_supported=try_get(app.systems.get_processors),
simple_storage_supported=try_get(
app.systems.get_simple_storage_collection),
total_cpus=try_get(app.systems.get_total_cpus),
boot_source_target=app.systems.get_boot_device(identity),
boot_source_mode=try_get(app.systems.get_boot_mode),

View File

@@ -63,22 +63,30 @@
"HealthRollUp": "OK"
}
},
{%- if bios_supported %}
"Bios": {
"@odata.id": {{ "/redfish/v1/Systems/%s/BIOS"|format(identity)|tojson }}
},
{%- endif %}
{%- if bios_version %}
"BiosVersion": {{ bios_version|string|tojson }},
{%- endif %}
{%- if processors_supported %}
"Processors": {
"@odata.id": {{ "/redfish/v1/Systems/%s/Processors"|format(identity)|tojson }}
},
{%- endif %}
"Memory": {
"@odata.id": {{ "/redfish/v1/Systems/%s/Memory"|format(identity)|tojson }}
},
"SecureBoot": {
"@odata.id": {{ "/redfish/v1/Systems/%s/SecureBoot"|format(identity)|tojson }}
},
{%- if simple_storage_supported %}
"SimpleStorage": {
"@odata.id": {{ "/redfish/v1/Systems/%s/SimpleStorage"|format(identity)|tojson }}
},
{%- endif %}
"Storage": {
"@odata.id": {{ "/redfish/v1/Systems/%s/Storage"|format(identity)|tojson }}
},

View File

@@ -573,6 +573,191 @@ class BiosTestCase(EmulatorTestCase):
systems_mock.return_value.reset_bios.assert_called_once_with(self.uuid)
class ConditionalResourceAdvertisingTestCase(EmulatorTestCase):
"""Test that resources are only advertised when driver supports them"""
def setUp(self):
super(ConditionalResourceAdvertisingTestCase, self).setUp()
self.set_feature_set("full")
@patch_resource('indicators')
@patch_resource('chassis')
@patch_resource('managers')
@patch_resource('systems')
def test_bios_advertised_when_supported(
self, systems_mock, managers_mock, chassis_mock, indicators_mock):
"""Test BIOS is advertised when driver supports it"""
systems_mock = systems_mock.return_value
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
systems_mock.get_power_state.return_value = 'On'
systems_mock.get_boot_device.return_value = 'Cd'
systems_mock.get_boot_mode.side_effect = error.NotSupportedError
systems_mock.get_bios.return_value = {'attr1': 'value1'}
systems_mock.get_processors.side_effect = error.NotSupportedError
systems_mock.get_simple_storage_collection.side_effect = (
error.NotSupportedError)
systems_mock.get_total_memory.side_effect = error.NotSupportedError
systems_mock.get_total_cpus.side_effect = error.NotSupportedError
systems_mock.get_versions.side_effect = error.NotSupportedError
managers_mock.return_value.get_managers_for_system.return_value = []
chassis_mock.return_value.chassis = []
indicators_mock.return_value.get_indicator_state.return_value = 'Off'
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz')
self.assertEqual(200, response.status_code)
self.assertIn('Bios', response.json)
self.assertEqual(
'/redfish/v1/Systems/xxxx-yyyy-zzzz/BIOS',
response.json['Bios']['@odata.id'])
@patch_resource('indicators')
@patch_resource('chassis')
@patch_resource('managers')
@patch_resource('systems')
def test_bios_not_advertised_when_not_supported(
self, systems_mock, managers_mock, chassis_mock, indicators_mock):
"""Test BIOS is NOT advertised when driver doesn't support it"""
systems_mock = systems_mock.return_value
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
systems_mock.get_power_state.return_value = 'On'
systems_mock.get_boot_device.return_value = 'Cd'
systems_mock.get_boot_mode.side_effect = error.NotSupportedError
systems_mock.get_bios.side_effect = error.NotSupportedError
systems_mock.get_processors.side_effect = error.NotSupportedError
systems_mock.get_simple_storage_collection.side_effect = (
error.NotSupportedError)
systems_mock.get_total_memory.side_effect = error.NotSupportedError
systems_mock.get_total_cpus.side_effect = error.NotSupportedError
systems_mock.get_versions.side_effect = error.NotSupportedError
managers_mock.return_value.get_managers_for_system.return_value = []
chassis_mock.return_value.chassis = []
indicators_mock.return_value.get_indicator_state.return_value = 'Off'
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz')
self.assertEqual(200, response.status_code)
self.assertNotIn('Bios', response.json)
@patch_resource('indicators')
@patch_resource('chassis')
@patch_resource('managers')
@patch_resource('systems')
def test_processors_advertised_when_supported(
self, systems_mock, managers_mock, chassis_mock, indicators_mock):
"""Test Processors is advertised when driver supports it"""
systems_mock = systems_mock.return_value
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
systems_mock.get_power_state.return_value = 'On'
systems_mock.get_boot_device.return_value = 'Cd'
systems_mock.get_boot_mode.side_effect = error.NotSupportedError
systems_mock.get_processors.return_value = [{'id': 'CPU'}]
systems_mock.get_bios.side_effect = error.NotSupportedError
systems_mock.get_simple_storage_collection.side_effect = (
error.NotSupportedError)
systems_mock.get_total_memory.side_effect = error.NotSupportedError
systems_mock.get_total_cpus.side_effect = error.NotSupportedError
systems_mock.get_versions.side_effect = error.NotSupportedError
managers_mock.return_value.get_managers_for_system.return_value = []
chassis_mock.return_value.chassis = []
indicators_mock.return_value.get_indicator_state.return_value = 'Off'
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz')
self.assertEqual(200, response.status_code)
self.assertIn('Processors', response.json)
self.assertEqual(
'/redfish/v1/Systems/xxxx-yyyy-zzzz/Processors',
response.json['Processors']['@odata.id'])
@patch_resource('indicators')
@patch_resource('chassis')
@patch_resource('managers')
@patch_resource('systems')
def test_processors_not_advertised_when_not_supported(
self, systems_mock, managers_mock, chassis_mock, indicators_mock):
"""Test Processors is NOT advertised when driver doesn't support it"""
systems_mock = systems_mock.return_value
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
systems_mock.get_power_state.return_value = 'On'
systems_mock.get_boot_device.return_value = 'Cd'
systems_mock.get_boot_mode.side_effect = error.NotSupportedError
systems_mock.get_processors.side_effect = error.NotSupportedError
systems_mock.get_bios.side_effect = error.NotSupportedError
systems_mock.get_simple_storage_collection.side_effect = (
error.NotSupportedError)
systems_mock.get_total_memory.side_effect = error.NotSupportedError
systems_mock.get_total_cpus.side_effect = error.NotSupportedError
systems_mock.get_versions.side_effect = error.NotSupportedError
managers_mock.return_value.get_managers_for_system.return_value = []
chassis_mock.return_value.chassis = []
indicators_mock.return_value.get_indicator_state.return_value = 'Off'
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz')
self.assertEqual(200, response.status_code)
self.assertNotIn('Processors', response.json)
@patch_resource('indicators')
@patch_resource('chassis')
@patch_resource('managers')
@patch_resource('systems')
def test_simple_storage_advertised_when_supported(
self, systems_mock, managers_mock, chassis_mock, indicators_mock):
"""Test SimpleStorage is advertised when driver supports it"""
systems_mock = systems_mock.return_value
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
systems_mock.get_power_state.return_value = 'On'
systems_mock.get_boot_device.return_value = 'Cd'
systems_mock.get_boot_mode.side_effect = error.NotSupportedError
systems_mock.get_simple_storage_collection.return_value = (
{'virtio': {}})
systems_mock.get_bios.side_effect = error.NotSupportedError
systems_mock.get_processors.side_effect = error.NotSupportedError
systems_mock.get_total_memory.side_effect = error.NotSupportedError
systems_mock.get_total_cpus.side_effect = error.NotSupportedError
systems_mock.get_versions.side_effect = error.NotSupportedError
managers_mock.return_value.get_managers_for_system.return_value = []
chassis_mock.return_value.chassis = []
indicators_mock.return_value.get_indicator_state.return_value = 'Off'
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz')
self.assertEqual(200, response.status_code)
self.assertIn('SimpleStorage', response.json)
self.assertEqual(
'/redfish/v1/Systems/xxxx-yyyy-zzzz/SimpleStorage',
response.json['SimpleStorage']['@odata.id'])
@patch_resource('indicators')
@patch_resource('chassis')
@patch_resource('managers')
@patch_resource('systems')
def test_simple_storage_not_advertised_when_not_supported(
self, systems_mock, managers_mock, chassis_mock, indicators_mock):
"""Test SimpleStorage NOT advertised when driver doesn't support"""
systems_mock = systems_mock.return_value
systems_mock.uuid.return_value = 'zzzz-yyyy-xxxx'
systems_mock.get_power_state.return_value = 'On'
systems_mock.get_boot_device.return_value = 'Cd'
systems_mock.get_boot_mode.side_effect = error.NotSupportedError
systems_mock.get_simple_storage_collection.side_effect = (
error.NotSupportedError)
systems_mock.get_bios.side_effect = error.NotSupportedError
systems_mock.get_processors.side_effect = error.NotSupportedError
systems_mock.get_total_memory.side_effect = error.NotSupportedError
systems_mock.get_total_cpus.side_effect = error.NotSupportedError
systems_mock.get_versions.side_effect = error.NotSupportedError
managers_mock.return_value.get_managers_for_system.return_value = []
chassis_mock.return_value.chassis = []
indicators_mock.return_value.get_indicator_state.return_value = 'Off'
response = self.app.get('/redfish/v1/Systems/xxxx-yyyy-zzzz')
self.assertEqual(200, response.status_code)
self.assertNotIn('SimpleStorage', response.json)
@patch_resource('systems')
class EthernetInterfacesTestCase(EmulatorTestCase):