Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-sushy for openSUSE:Factory checked in at 2026-03-04 21:09:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-sushy (Old) and /work/SRC/openSUSE:Factory/.python-sushy.new.561 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-sushy" Wed Mar 4 21:09:26 2026 rev:21 rq:1336287 version:5.10.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-sushy/python-sushy.changes 2025-11-24 14:10:50.377432424 +0100 +++ /work/SRC/openSUSE:Factory/.python-sushy.new.561/python-sushy.changes 2026-03-04 21:10:14.060887173 +0100 @@ -1,0 +2,11 @@ +Wed Mar 4 08:30:54 UTC 2026 - Dirk Müller <[email protected]> + +- update to 5.10.0: + * skip missing collection members instead of failing + * Add read\_timeout and connect\_timeout parameters for faster + BMC failure + * Don't require Boot and Actions for Systems + * Add complete LLDP fields to Port.Ethernet.LLDPReceive per + DMTF Redfish v1.12.0 + +------------------------------------------------------------------- Old: ---- sushy-5.8.0.tar.gz New: ---- sushy-5.10.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-sushy.spec ++++++ --- /var/tmp/diff_new_pack.M4fS9h/_old 2026-03-04 21:10:14.840919411 +0100 +++ /var/tmp/diff_new_pack.M4fS9h/_new 2026-03-04 21:10:14.840919411 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-sushy # -# Copyright (c) 2025 SUSE LLC and contributors +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,7 +17,7 @@ Name: python-sushy -Version: 5.8.0 +Version: 5.10.0 Release: 0 Summary: Python library to communicate with Redfish based systems License: Apache-2.0 ++++++ sushy-5.8.0.tar.gz -> sushy-5.10.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/AUTHORS new/sushy-5.10.0/AUTHORS --- old/sushy-5.8.0/AUTHORS 2025-11-13 14:55:50.000000000 +0100 +++ new/sushy-5.10.0/AUTHORS 2026-02-23 11:14:54.000000000 +0100 @@ -42,8 +42,10 @@ Lucas Alvares Gomes <[email protected]> Manuel Schönlaub <[email protected]> Manuel Schönlaub <[email protected]> +Marcus Furlong <[email protected]> Mark Goddard <[email protected]> Milan Fencik <[email protected]> +Nahian Pathan <[email protected]> Nate Potter <[email protected]> Nguyen Van Trung <[email protected]> Nidhi Rai <[email protected]> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/ChangeLog new/sushy-5.10.0/ChangeLog --- old/sushy-5.8.0/ChangeLog 2025-11-13 14:55:50.000000000 +0100 +++ new/sushy-5.10.0/ChangeLog 2026-02-23 11:14:54.000000000 +0100 @@ -1,6 +1,18 @@ CHANGES ======= +5.10.0 +------ + +* skip missing collection members instead of failing +* Add read\_timeout and connect\_timeout parameters for faster BMC failure +* Don't require Boot and Actions for Systems + +5.9.0 +----- + +* Add complete LLDP fields to Port.Ethernet.LLDPReceive per DMTF Redfish v1.12.0 + 5.8.0 ----- @@ -9,6 +21,7 @@ * Add comprehensive PCIe function support to Sushy * Check required credentials in a detailed way * Remove support for Python 3.9 +* Support expanded Chassis and Storage for redfish * Add comprehensive PCIe device support to Sushy * Restore flake8-import-order * Improve Dell Asynchronous task handling diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/PKG-INFO new/sushy-5.10.0/PKG-INFO --- old/sushy-5.8.0/PKG-INFO 2025-11-13 14:55:51.027289200 +0100 +++ new/sushy-5.10.0/PKG-INFO 2026-02-23 11:14:54.779126400 +0100 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: sushy -Version: 5.8.0 +Version: 5.10.0 Summary: Sushy is a small Python library to communicate with Redfish based systems Home-page: https://docs.openstack.org/sushy/latest/ Author: OpenStack @@ -23,6 +23,15 @@ Requires-Dist: requests>=2.14.2 Requires-Dist: python-dateutil>=2.7.0 Requires-Dist: stevedore>=1.29.0 +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license-file +Dynamic: requires-dist +Dynamic: requires-python +Dynamic: summary Overview ======== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/releasenotes/notes/add-connection-timeout-param-d6c403e7809267bc.yaml new/sushy-5.10.0/releasenotes/notes/add-connection-timeout-param-d6c403e7809267bc.yaml --- old/sushy-5.8.0/releasenotes/notes/add-connection-timeout-param-d6c403e7809267bc.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/sushy-5.10.0/releasenotes/notes/add-connection-timeout-param-d6c403e7809267bc.yaml 2026-02-23 11:14:11.000000000 +0100 @@ -0,0 +1,21 @@ +--- +features: + - | + Adds two new timeout parameters to the ``Sushy`` class constructor to + allow faster failure when a BMC is unreachable: + + * ``read_timeout``: HTTP read timeout in seconds. This is the maximum + time to wait for a response from the BMC after the connection is + established. Defaults to 60 seconds (unchanged from previous behavior). + + * ``connect_timeout``: TCP connection timeout in seconds. This is the + maximum time to wait for the initial TCP connection to the BMC to be + established. When specified, sushy uses separate connect and read + timeouts, allowing faster failure when a BMC is unreachable while + still allowing longer read timeouts for slow BMCs. If not specified, + ``read_timeout`` is used for both. + + This change is fully backwards compatible. Setting ``connect_timeout`` + to a lower value (e.g., 10 seconds) significantly reduces the time + spent waiting when a BMC is unreachable, which previously could take + up to 60 seconds per connection attempt. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/releasenotes/notes/add-lldp-receive-fields-enhancement-f1e3d4a5b2c6d789.yaml new/sushy-5.10.0/releasenotes/notes/add-lldp-receive-fields-enhancement-f1e3d4a5b2c6d789.yaml --- old/sushy-5.8.0/releasenotes/notes/add-lldp-receive-fields-enhancement-f1e3d4a5b2c6d789.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/sushy-5.10.0/releasenotes/notes/add-lldp-receive-fields-enhancement-f1e3d4a5b2c6d789.yaml 2026-02-23 11:14:11.000000000 +0100 @@ -0,0 +1,22 @@ +--- +features: + - | + Enhances the existing LLDPReceiveField class by adding new LLDP receive data + fields as specified in Redfish Port Schema v1.12.0. The existing class already + contained chassis_id and port_id fields, and this enhancement adds 9 additional + LLDP receive properties. + + New LLDP receive fields added: + - chassis_id_subtype: IEEE 802 chassis ID subtype identification + - port_id_subtype: IEEE 802 port ID subtype with MAC address handling + - system_name: System name received from remote link partner + - system_description: System description from remote link partner + - system_capabilities: Network device capabilities mapping + - management_address_ipv4: IPv4 management address + - management_address_ipv6: IPv6 management address + - management_address_mac: MAC management address + - management_vlan_id: Management VLAN ID configuration (0-4095) + + This enhancement provides LLDP receive information through + the EthernetField.lldp_receive property, enabling better network topology + discovery. \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/releasenotes/notes/do_not_require_actions_or_boot-b495d5407666be8b.yaml new/sushy-5.10.0/releasenotes/notes/do_not_require_actions_or_boot-b495d5407666be8b.yaml --- old/sushy-5.8.0/releasenotes/notes/do_not_require_actions_or_boot-b495d5407666be8b.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/sushy-5.10.0/releasenotes/notes/do_not_require_actions_or_boot-b495d5407666be8b.yaml 2026-02-23 11:14:11.000000000 +0100 @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixes sushy library compatibility for Nvidia HGX systems where the + Boot subfield and actions list may be missing from the ComputerSystem + resource. These fields, per current DMTF schema do not appear to be + required. More information can be found in + `bug 2131954 <https://bugs.launchpad.net/sushy/+bug/2131942>`_. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/releasenotes/notes/handle-missing-collection-members-8ccbd4588790eabe.yaml new/sushy-5.10.0/releasenotes/notes/handle-missing-collection-members-8ccbd4588790eabe.yaml --- old/sushy-5.8.0/releasenotes/notes/handle-missing-collection-members-8ccbd4588790eabe.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/sushy-5.10.0/releasenotes/notes/handle-missing-collection-members-8ccbd4588790eabe.yaml 2026-02-23 11:14:11.000000000 +0100 @@ -0,0 +1,9 @@ +--- +fixes: + - | + Some Redfish implementations advertise collection members in their JSON + responses that don't actually exist. For example, the HGX board lists + LogServices/FDR as a member, but returns 404 when sushy tries to fetch it. + This causes sushy to fail during System initialization. We now handle + those gracefully by allowing get_members() to catch ResourceNotFoundError + for individual members. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/connector.py new/sushy-5.10.0/sushy/connector.py --- old/sushy-5.8.0/sushy/connector.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/connector.py 2026-02-23 11:14:11.000000000 +0100 @@ -45,7 +45,8 @@ self, url, username=None, password=None, verify=True, response_callback=None, server_side_retries=0, server_side_retries_delay=0, - default_request_timeout=60): + default_request_timeout=60, + connect_timeout=None): self._url = url self._verify = verify self._session = requests.Session() @@ -54,6 +55,10 @@ self._server_side_retries = server_side_retries self._server_side_retries_delay = server_side_retries_delay self._default_request_timeout = default_request_timeout + # If connect_timeout is specified, use a tuple (connect, read) for + # requests timeout. This allows faster failure when a BMC is + # unreachable while still allowing longer read timeouts for slow BMCs. + self._connect_timeout = connect_timeout # NOTE(TheJulia): In order to help prevent recursive post operations # by allowing us to understand that we should stop authentication. @@ -140,6 +145,10 @@ server_side_retries_left = self._server_side_retries timeout = timeout or self._default_request_timeout + # If connect_timeout is configured, use a tuple (connect, read) for + # the requests timeout to allow faster failure on unreachable BMCs. + if self._connect_timeout is not None: + timeout = (self._connect_timeout, timeout) url = path if urlparse.urlparse(path).netloc else urlparse.urljoin( self._url, path) @@ -324,7 +333,13 @@ raise exceptions.ConnectionError(url=url, error=m) mon = TaskMonitor.from_response(self, response, path) - mon.wait(timeout) + # TaskMonitor.wait() expects a single number, not a tuple. + # If timeout is a tuple (connect, read), use the read timeout. + if isinstance(timeout, tuple): + wait_timeout = timeout[1] + else: + wait_timeout = timeout + mon.wait(wait_timeout) response = mon.response exceptions.raise_for_response(method, url, response) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/main.py new/sushy-5.10.0/sushy/main.py --- old/sushy-5.8.0/sushy/main.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/main.py 2026-02-23 11:14:11.000000000 +0100 @@ -159,7 +159,9 @@ auth=None, connector=None, public_connector=None, language='en', server_side_retries=10, - server_side_retries_delay=3): + server_side_retries_delay=3, + read_timeout=60, + connect_timeout=None): """A class representing a RootService :param base_url: The base URL to the Redfish controller. It @@ -186,6 +188,14 @@ case of server side errors. Defaults to 10. :param server_side_retries_delay: Time in seconds between retries of GET requests in case of server side errors. Defaults to 3. + :param read_timeout: HTTP read timeout in seconds. This is the maximum + time to wait for a response from the BMC after the connection is + established. Defaults to 60. + :param connect_timeout: TCP connection timeout in seconds. This is + the maximum time to wait for the initial TCP connection to the + BMC to be established. If not specified, read_timeout is used for + both connect and read timeouts. Setting this to a lower value + (e.g., 10) allows faster failure when a BMC is unreachable. """ self._root_prefix = root_prefix if (auth is not None and (password is not None @@ -202,7 +212,9 @@ connector or sushy_connector.Connector( base_url, verify=verify, server_side_retries=server_side_retries, - server_side_retries_delay=server_side_retries_delay), + server_side_retries_delay=server_side_retries_delay, + default_request_timeout=read_timeout, + connect_timeout=connect_timeout), path=self._root_prefix) self._public_connector = public_connector or requests self._language = language diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/resources/base.py new/sushy-5.10.0/sushy/resources/base.py --- old/sushy-5.8.0/sushy/resources/base.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/resources/base.py 2026-02-23 11:14:11.000000000 +0100 @@ -688,15 +688,19 @@ if not self._is_stale and not force: return + data_source = "" if json_doc: self._json = json_doc + data_source = "from expanded document" else: self._json = self._reader.get_data().json_doc attributes = self._parse_attributes(self._json) - LOG.debug('Received representation of %(type)s %(path)s: %(json)s', + LOG.debug('Received representation of %(type)s %(path)s%(source)s: ' + '%(json)s', {'type': self.__class__.__name__, 'path': self._path, + 'source': data_source, 'json': (attributes if self._log_resource_body else '<stripped>')}) self._do_refresh(force) @@ -843,7 +847,13 @@ :returns: A list of ``_resource_type`` objects """ - return [self.get_member(id_) for id_ in self.members_identities] + members = [] + for id_ in self.members_identities: + try: + members.append(self.get_member(id_)) + except exceptions.ResourceNotFoundError: + LOG.warning('Skipping missing collection member %s', id_) + return members class ResourceCollectionBase(ResourceLinksBase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/resources/chassis/chassis.py new/sushy-5.10.0/sushy/resources/chassis/chassis.py --- old/sushy-5.8.0/sushy/resources/chassis/chassis.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/resources/chassis/chassis.py 2026-02-23 11:14:11.000000000 +0100 @@ -140,8 +140,15 @@ _actions = ActionsField('Actions') + # Optional fields for expanded chassis data + power_data = base.Field('Power') + """Raw power data when chassis is retrieved with $expand""" + + thermal_data = base.Field('Thermal') + """Raw thermal data when chassis is retrieved with $expand""" + def __init__(self, connector, identity, redfish_version=None, - registries=None, root=None): + registries=None, root=None, json_doc=None): """A class representing a Chassis :param connector: A Connector instance @@ -151,10 +158,27 @@ :param registries: Dict of Redfish Message Registry objects to be used in any resource that needs registries to parse messages :param root: Sushy root object. Empty for Sushy root itself. + :param json_doc: parsed JSON document in form of Python types. """ super().__init__( connector, identity, redfish_version=redfish_version, - registries=registries, root=root) + registries=registries, root=root, json_doc=json_doc) + + def _get_expanded_data(self, field_name): + """Get expanded data for a field, returning None if only a reference. + + :param field_name: Name of the field to get expanded data for + :returns: Expanded data dict or None if unexpanded/reference-only + """ + expanded_data = self._json.get(field_name) if self._json else None + + # If expanded data only contains a reference (@odata.id), + # treat as unexpanded + if (expanded_data and isinstance(expanded_data, dict) + and list(expanded_data.keys()) == ['@odata.id']): + return None + + return expanded_data def _get_reset_action_element(self): reset_action = self._actions.reset @@ -274,7 +298,9 @@ self._conn, utils.get_sub_resource_path_by(self, 'Power'), redfish_version=self.redfish_version, registries=self.registries, - root=self.root) + root=self.root, + json_doc=self._get_expanded_data('Power') + ) @property @utils.cache_it @@ -289,7 +315,9 @@ self._conn, utils.get_sub_resource_path_by(self, 'Thermal'), redfish_version=self.redfish_version, registries=self.registries, - root=self.root) + root=self.root, + json_doc=self._get_expanded_data('Thermal') + ) @property @utils.cache_it diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/resources/system/network/constants.py new/sushy-5.10.0/sushy/resources/system/network/constants.py --- old/sushy-5.8.0/sushy/resources/system/network/constants.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/resources/system/network/constants.py 2026-02-23 11:14:12.000000000 +0100 @@ -137,3 +137,71 @@ TRAINING = 'Training' """This physical link on this interface is training.""" + + +class IEEE802IdSubtype(enum.Enum): + """IEEE 802.1AB ID Subtypes for Chassis ID and Port ID. + + Based on IEEE 802.1AB-2009 and DMTF Redfish Port Schema v1.12.0 + """ + + CHASSIS_COMP = 'ChassisComp' + """Chassis component, based on entPhysicalAlias in RFC4133.""" + + IF_ALIAS = 'IfAlias' + """Interface alias, based on the ifAlias MIB object.""" + + PORT_COMP = 'PortComp' + """Port component, based on entPhysicalAlias in RFC4133.""" + + MAC_ADDR = 'MacAddr' + """MAC address, based on agent-detected unicast source (IEEE 802).""" + + NETWORK_ADDR = 'NetworkAddr' + """Network address, based on agent-detected network address.""" + + IF_NAME = 'IfName' + """Interface name, based on the ifName MIB object.""" + + AGENT_ID = 'AgentId' + """Agent circuit ID, based on agent-local identifier (RFC3046).""" + + LOCAL_ASSIGN = 'LocalAssign' + """Locally assigned, based on alphanumeric value.""" + + NOT_TRANSMITTED = 'NotTransmitted' + """No data to be sent to/received from remote partner.""" + + +class LLDPSystemCapabilities(enum.Enum): + """LLDP System Capabilities. + + Based on IEEE 802.1AB and DMTF Redfish Port Schema v1.12.0 + """ + + NONE = 'None' + """System capabilities are transmitted, but no capabilities are set.""" + + BRIDGE = 'Bridge' + """'bridge' capability.""" + + DOCSIS_CABLE_DEVICE = 'DOCSISCableDevice' + """'DOCSIS cable device' capability.""" + + OTHER = 'Other' + """'other' capability.""" + + REPEATER = 'Repeater' + """'repeater' capability.""" + + ROUTER = 'Router' + """'router' capability.""" + + STATION = 'Station' + """'station' capability.""" + + TELEPHONE = 'Telephone' + """'telephone' capability.""" + + WLAN_ACCESS_POINT = 'WLANAccessPoint' + """'WLAN access point' capability.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/resources/system/port.py new/sushy-5.10.0/sushy/resources/system/port.py --- old/sushy-5.8.0/sushy/resources/system/port.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/resources/system/port.py 2026-02-23 11:14:12.000000000 +0100 @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # This is referred from Redfish standard schema. -# https://redfish.dmtf.org/schemas/v1/Port.v1_8_0.json +# https://redfish.dmtf.org/schemas/v1/Port.v1_12_0.json from sushy.resources import base from sushy.resources import common @@ -18,12 +18,56 @@ class LLDPReceiveField(base.CompositeField): + """LLDP data being received on this link. + + Based on DMTF Redfish Port Schema v1.12.0 + https://redfish.dmtf.org/schemas/v1/Port.v1_12_0.json#/definitions/LLDPReceive + """ + chassis_id = base.Field("ChassisId") """chassis ID received from the remote partner across this link.""" + chassis_id_subtype = base.MappedField("ChassisIdSubtype", + constants.IEEE802IdSubtype) + """The type of identifier used for the chassis ID""" + port_id = base.Field("PortId") """A colon delimited string of hexadecimal octets identifying a port.""" + port_id_subtype = base.MappedField("PortIdSubtype", + constants.IEEE802IdSubtype) + """The port ID subtype received from the remote partner""" + + # TLV Type 3 - Time To Live not in current schema + + # TLV Type 4 - Port Description ,not in schema + + # TLV Type 5 - System Name + system_name = base.Field("SystemName") + """The system name received from the remote partner across this link.""" + + # TLV Type 6 - System Description + system_description = base.Field("SystemDescription") + """The system description received from the remote partner.""" + + # TLV Type 7 - System Capabilities + system_capabilities = base.MappedListField( + "SystemCapabilities", constants.LLDPSystemCapabilities) + """The system capabilities received from the remote partner.""" + + # TLV Type 8 - Management Addresses + management_address_ipv4 = base.Field("ManagementAddressIPv4") + """The IPv4 management address received from the remote partner.""" + + management_address_ipv6 = base.Field("ManagementAddressIPv6") + """The IPv6 management address received from the remote partner.""" + + management_address_mac = base.Field("ManagementAddressMAC") + """The management MAC address received from the remote partner.""" + + management_vlan_id = base.Field("ManagementVlanId", adapter=int) + """The management VLAN ID received from the remote partner (0-4095).""" + class EthernetField(base.CompositeField): associated_mac_addresses = base.Field( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/resources/system/simple_storage.py new/sushy-5.10.0/sushy/resources/system/simple_storage.py --- old/sushy-5.8.0/sushy/resources/system/simple_storage.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/resources/system/simple_storage.py 2026-02-23 11:14:12.000000000 +0100 @@ -30,6 +30,9 @@ name = base.Field('Name', required=True) """The name of the storage device""" + model = base.Field('Model') + """The model of the storage device""" + capacity_bytes = base.Field('CapacityBytes', adapter=utils.int_or_none) """The size of the storage device.""" @@ -62,6 +65,24 @@ def _resource_type(self): return SimpleStorage + @utils.cache_it + def get_members(self): + """Return SimpleStorage objects using expanded JSON when available.""" + members = [] + for member in self._json['Members']: + # If data only contains a reference (@odata.id), + # treat as unexpanded + if (isinstance(member, dict) + and list(member.keys()) == ['@odata.id']): + return super().get_members() + + simple_storage = SimpleStorage( + self._conn, member['@odata.id'], + json_doc=member, redfish_version=self.redfish_version, + registries=self.registries, root=self.root) + members.append(simple_storage) + return members + @property @utils.cache_it def disks_sizes_bytes(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/resources/system/storage/storage.py new/sushy-5.10.0/sushy/resources/system/storage/storage.py --- old/sushy-5.8.0/sushy/resources/system/storage/storage.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/resources/system/storage/storage.py 2026-02-23 11:14:12.000000000 +0100 @@ -163,6 +163,24 @@ def _resource_type(self): return Storage + @utils.cache_it + def get_members(self): + """Return Storage objects with expanded JSON data when available.""" + storage_members = [] + for member in self._json['Members']: + # If data only contains a reference (@odata.id), + # treat as unexpanded + if (isinstance(member, dict) + and list(member.keys()) == ['@odata.id']): + return super().get_members() + + storage = Storage( + self._conn, member['@odata.id'], + json_doc=member, redfish_version=self.redfish_version, + registries=self.registries, root=self.root) + storage_members.append(storage) + return storage_members + @property @utils.cache_it def drives_sizes_bytes(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/resources/system/system.py new/sushy-5.10.0/sushy/resources/system/system.py --- old/sushy-5.8.0/sushy/resources/system/system.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/resources/system/system.py 2026-02-23 11:14:12.000000000 +0100 @@ -42,6 +42,8 @@ LOG = logging.getLogger(__name__) +EXPAND_QUERY = '?$expand=.($levels=1)' + class ActionsField(base.CompositeField): reset = common.ResetActionField('#ComputerSystem.Reset') @@ -192,7 +194,7 @@ bios_version = base.Field('BiosVersion') """The system BIOS version""" - boot = BootField('Boot', required=True) + boot = BootField('Boot') """A dictionary containing the current boot device, frequency and mode""" description = base.Field('Description') @@ -252,7 +254,7 @@ """ _supermicro_models_cd_vmedia = frozenset(['ars-111gl-nhr']) - _actions = ActionsField('Actions', required=True) + _actions = ActionsField('Actions') boot_progress = BootProgressField('BootProgress') """The last updated boot progress indicator""" @@ -276,8 +278,10 @@ root=root) def _get_reset_action_element(self): - reset_action = self._actions.reset - # TODO(dtantsur): make this check also declarative? + reset_action = None + if self._actions: + reset_action = self._actions.reset + # TODO(dtantsur): make this check also declarative? if not reset_action: raise exceptions.MissingActionError(action='#ComputerSystem.Reset', resource=self._path) @@ -322,7 +326,7 @@ :returns: A set with the allowed values. """ - if not self.boot.allowed_values: + if not self.boot or not self.boot.allowed_values: LOG.warning('Could not figure out the allowed values for ' 'configuring the boot source for System %s', self.identity) @@ -390,6 +394,7 @@ and self.model.lower() not in self._supermicro_models_cd_vmedia and target == sys_cons.BootSource.CD + and self.boot and sys_cons.BootSource.USB_CD.value in self.boot.allowed_values): LOG.debug('Boot from vMedia was requested on a SuperMicro' @@ -571,7 +576,31 @@ :returns: `SimpleStorageCollection` instance """ return sys_simple_storage.SimpleStorageCollection( - self._conn, utils.get_sub_resource_path_by(self, "SimpleStorage"), + self._conn, + utils.get_sub_resource_path_by(self, "SimpleStorage"), + redfish_version=self.redfish_version, + registries=self.registries, root=self.root) + + @property + @utils.cache_it + def simple_storage_expanded(self): + """A collection of simple storage with devices expanded. + + This returns a reference to `SimpleStorageCollection` instance with + expanded data retrieved in a single request. + + It is set once when the first time it is queried. On refresh, + this property is marked as stale (greedy-refresh not done). + + :returns: `SimpleStorageCollection` instance with expanded data + :raises: MissingAttributeError if 'SimpleStorage/@odata.id' field + is missing. + """ + path = utils.get_sub_resource_path_by(self, "SimpleStorage") + path = f'{path}{EXPAND_QUERY}' + + return sys_simple_storage.SimpleStorageCollection( + self._conn, path, redfish_version=self.redfish_version, registries=self.registries, root=self.root) @@ -594,7 +623,32 @@ :returns: `StorageCollection` instance """ return sys_storage.StorageCollection( - self._conn, utils.get_sub_resource_path_by(self, "Storage"), + self._conn, + utils.get_sub_resource_path_by(self, "Storage"), + redfish_version=self.redfish_version, + registries=self.registries, root=self.root) + + @property + @utils.cache_it + def storage_expanded(self): + """A collection of storage subsystems with expanded data. + + This returns a reference to `StorageCollection` instance with + additional data (e.g controllers and drives) expanded in a single + request. + + It is set once when the first time it is queried. On refresh, + this property is marked as stale (greedy-refresh not done). + + :returns: `StorageCollection` instance with expanded data + :raises: MissingAttributeError if 'Storage/@odata.id' field + is missing. + """ + path = utils.get_sub_resource_path_by(self, "Storage") + path = f'{path}{EXPAND_QUERY}' + + return sys_storage.StorageCollection( + self._conn, path, redfish_version=self.redfish_version, registries=self.registries, root=self.root) @@ -645,6 +699,26 @@ return [chassis.Chassis(self._conn, path, redfish_version=self.redfish_version, + registries=self.registries, root=self.root) + for path in paths] + + @property + @utils.cache_it + def chassis_expanded(self): + """A list of chassis with expanded data. + + Returns a list of `Chassis` objects with additional data + (e.g thermal and power) expanded in a single request. + + :raises: MissingAttributeError if '@odata.id' field is missing. + :returns: A list of `Chassis` instances with expanded data + """ + paths = utils.get_sub_resource_path_by( + self, ["Links", "Chassis"], is_collection=True) + paths = [f'{path}{EXPAND_QUERY}' for path in paths] + + return [chassis.Chassis(self._conn, path, + redfish_version=self.redfish_version, registries=self.registries, root=self.root) for path in paths] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/json_samples/port.json new/sushy-5.10.0/sushy/tests/unit/json_samples/port.json --- old/sushy-5.8.0/sushy/tests/unit/json_samples/port.json 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/json_samples/port.json 2026-02-23 11:14:12.000000000 +0100 @@ -13,10 +13,17 @@ "FlowControlConfiguration": "None", "FlowControlStatus": "None", "LLDPReceive": { - "ChassisId": "Not Available", - "ChassisIdSubtype": null, + "ChassisId": "c4:7e:e0:e4:55:3f", + "ChassisIdSubtype": "MacAddr", "PortId": "0A:1B:2C:3D:4E:5F:6A:7B:8C:9D:0E:1F:2A", - "PortIdSubtype": null + "PortIdSubtype": "IfName", + "SystemName": "switch-00.example.com", + "SystemDescription": "Test Software, Version 00.00.00", + "SystemCapabilities": ["Bridge", "Router"], + "ManagementAddressIPv4": "192.168.1.1", + "ManagementAddressIPv6": "fe80::1", + "ManagementAddressMAC": "c4:7e:e0:e4:55:40", + "ManagementVlanId": 100 }, "WakeOnLANEnabled": false }, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/json_samples/simple_storage_collection_expanded.json new/sushy-5.10.0/sushy/tests/unit/json_samples/simple_storage_collection_expanded.json --- old/sushy-5.8.0/sushy/tests/unit/json_samples/simple_storage_collection_expanded.json 1970-01-01 01:00:00.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/json_samples/simple_storage_collection_expanded.json 2026-02-23 11:14:12.000000000 +0100 @@ -0,0 +1,40 @@ +{ + "@odata.type": "#SimpleStorageCollection.SimpleStorageCollection", + "@odata.id": "/redfish/v1/Systems/1/SimpleStorage", + "Name": "Simple Storage Collection", + "[email protected]": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/1/SimpleStorage/RAID-1", + "@odata.type": "#SimpleStorage.v1_3_2.SimpleStorage", + "Id": "RAID-1", + "Name": "RAID Controller", + "Status": {"Health": "OK", "State": "Enabled"}, + "Devices": [ + { + "Name": "Drive 1", + "Model": "SSD-500GB", + "CapacityBytes": 500000000000, + "Status": {"Health": "OK", "State": "Enabled"} + }, + { + "Name": "Drive 2", + "Model": "SSD-1TB", + "CapacityBytes": 1000000000000, + "Status": {"Health": "OK", "State": "Enabled"} + } + ], + "[email protected]": 2 + }, + { + "@odata.id": "/redfish/v1/Systems/1/SimpleStorage/AHCI-1", + "@odata.type": "#SimpleStorage.v1_3_2.SimpleStorage", + "Id": "AHCI-1", + "Name": "AHCI Controller", + "Status": {"Health": "OK", "State": "Enabled"}, + "Devices": [], + "[email protected]": 0 + } + ], + "@odata.context": "/redfish/v1/$metadata#SimpleStorageCollection.SimpleStorageCollection" +} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/json_samples/storage_collection_expanded.json new/sushy-5.10.0/sushy/tests/unit/json_samples/storage_collection_expanded.json --- old/sushy-5.8.0/sushy/tests/unit/json_samples/storage_collection_expanded.json 1970-01-01 01:00:00.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/json_samples/storage_collection_expanded.json 2026-02-23 11:14:12.000000000 +0100 @@ -0,0 +1,62 @@ +{ + "@odata.type": "#StorageCollection.StorageCollection", + "@odata.id": "/redfish/v1/Systems/1/Storage", + "Name": "Storage Collection", + "[email protected]": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/1/Storage/RAID-1", + "@odata.type": "#Storage.v1_16_0.Storage", + "Id": "RAID-1", + "Name": "RAID Controller", + "Status": {"Health": "OK", "State": "Enabled"}, + "StorageControllers": [ + { + "MemberId": "0", + "Name": "LSI MegaRAID SAS 9361-8i", + "Manufacturer": "LSI", + "Model": "MegaRAID SAS 9361-8i", + "SerialNumber": "SV12345678", + "PartNumber": "LSI00417", + "Status": {"Health": "OK", "State": "Enabled"}, + "SupportedRAIDTypes": ["RAID0", "RAID1", "RAID5", "RAID6", "RAID10"], + "SpeedGbps": 12.0 + } + ], + "Drives": [ + {"@odata.id": "/redfish/v1/Systems/1/Storage/RAID-1/Drives/Drive1"}, + {"@odata.id": "/redfish/v1/Systems/1/Storage/RAID-1/Drives/Drive2"} + ], + "Volumes": { + "@odata.id": "/redfish/v1/Systems/1/Storage/RAID-1/Volumes" + } + }, + { + "@odata.id": "/redfish/v1/Systems/1/Storage/NVMe-1", + "@odata.type": "#Storage.v1_16_0.Storage", + "Id": "NVMe-1", + "Name": "NVMe Storage Controller", + "Status": {"Health": "OK", "State": "Enabled"}, + "StorageControllers": [ + { + "MemberId": "0", + "Name": "Samsung SSD 980 PRO", + "Manufacturer": "Samsung", + "Model": "SSD 980 PRO", + "SerialNumber": "S6J9NX0T123456A", + "PartNumber": "MZ-V8P1T0BW", + "Status": {"Health": "OK", "State": "Enabled"}, + "SupportedRAIDTypes": [], + "SpeedGbps": 32.0 + } + ], + "Drives": [ + {"@odata.id": "/redfish/v1/Systems/1/Storage/NVMe-1/Drives/Drive1"} + ], + "Volumes": { + "@odata.id": "/redfish/v1/Systems/1/Storage/NVMe-1/Volumes" + } + } + ], + "@odata.context": "/redfish/v1/$metadata#StorageCollection.StorageCollection" +} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/json_samples/storage_collection_multiple.json new/sushy-5.10.0/sushy/tests/unit/json_samples/storage_collection_multiple.json --- old/sushy-5.8.0/sushy/tests/unit/json_samples/storage_collection_multiple.json 1970-01-01 01:00:00.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/json_samples/storage_collection_multiple.json 2026-02-23 11:14:12.000000000 +0100 @@ -0,0 +1,16 @@ +{ + "@odata.type": "#StorageCollection.StorageCollection", + "Name": "Storage Collection", + "[email protected]": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/2" + } + ], + "@odata.context": "/redfish/v1/$metadata#StorageCollection.StorageCollection", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage", + "@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/json_samples/system.json new/sushy-5.10.0/sushy/tests/unit/json_samples/system.json --- old/sushy-5.8.0/sushy/tests/unit/json_samples/system.json 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/json_samples/system.json 2026-02-23 11:14:12.000000000 +0100 @@ -111,6 +111,9 @@ "SimpleStorage": { "@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage" }, + "Storage": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage" + }, "LogServices": { "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices" }, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/resources/chassis/test_chassis.py new/sushy-5.10.0/sushy/tests/unit/resources/chassis/test_chassis.py --- old/sushy-5.8.0/sushy/tests/unit/resources/chassis/test_chassis.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/resources/chassis/test_chassis.py 2026-02-23 11:14:12.000000000 +0100 @@ -221,6 +221,26 @@ self.assertIsInstance(self.chassis.network_adapters, adapter.NetworkAdapterCollection) + def test_get_expanded_data_with_full_data(self): + # Test that expanded data is returned when present + expanded_data = {'Key': [{'Name': 'Val'}], 'List': []} + self.chassis._json = {'Power': expanded_data} + result = self.chassis._get_expanded_data('Power') + self.assertEqual(expanded_data, result) + + def test_get_expanded_data_with_reference_only(self): + # Test that None is returned for reference-only data + reference_data = {'@odata.id': '/redfish/v1/Chassis/1/Power'} + self.chassis._json = {'Power': reference_data} + result = self.chassis._get_expanded_data('Power') + self.assertIsNone(result) + + def test_get_expanded_data_missing_field(self): + # Test that None is returned when field is missing + self.chassis._json = {} + result = self.chassis._get_expanded_data('Power') + self.assertIsNone(result) + class ChassisCollectionTestCase(base.TestCase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/resources/system/storage/test_storage.py new/sushy-5.10.0/sushy/tests/unit/resources/system/storage/test_storage.py --- old/sushy-5.8.0/sushy/tests/unit/resources/system/storage/test_storage.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/resources/system/storage/test_storage.py 2026-02-23 11:14:12.000000000 +0100 @@ -390,3 +390,145 @@ self.conn.get.return_value.json.side_effect = successive_return_values self.assertEqual(1073741824000, self.stor_col.max_volume_size_bytes) + + @mock.patch.object(storage, 'Storage', autospec=True) + def test_get_members_with_multiple_storages(self, Storage_mock): + # Test collection with multiple storage members + with open('sushy/tests/unit/json_samples/' + 'storage_collection_multiple.json') as f: + multiple_storages_json = json.loads(f.read()) + + self.conn.get.return_value.json.return_value = multiple_storages_json + + # Create a new collection instance to get fresh data + stor_col_multi = storage.StorageCollection( + self.conn, '/redfish/v1/Systems/437XR1138R2/Storage', + redfish_version='1.0.2') + + members = stor_col_multi.get_members() + + # Should call Storage constructor for each member + expected_calls = [ + mock.call(self.conn, '/redfish/v1/Systems/437XR1138R2/Storage/1', + redfish_version='1.0.2', registries=None, root=None), + mock.call(self.conn, '/redfish/v1/Systems/437XR1138R2/Storage/2', + redfish_version='1.0.2', registries=None, root=None) + ] + Storage_mock.assert_has_calls(expected_calls) + self.assertEqual(2, len(members)) + + def test_get_members_expanded(self): + # Test with expanded JSON data containing full member objects + # This simulates a response from ?$expand=.($levels=1) + with open('sushy/tests/unit/json_samples/' + 'storage_collection_expanded.json') as f: + expanded_json = json.loads(f.read()) + + # Create collection with the URL that would have ?$expand appended + expanded_collection = storage.StorageCollection( + self.conn, + "/redfish/v1/Systems/1/Storage?$expand=.($levels=1)", + redfish_version="1.0.2", + ) + expanded_collection._json = expanded_json + + # Mock the Storage objects that will be created + mock_storage1 = mock.Mock(spec=storage.Storage) + mock_storage1.identity = "RAID-1" + mock_storage1.name = "RAID Controller" + + mock_storage2 = mock.Mock(spec=storage.Storage) + mock_storage2.identity = "NVMe-1" + mock_storage2.name = "NVMe Storage Controller" + + with mock.patch.object( + storage, "Storage", autospec=True + ) as mock_storage_cls: + # Make the constructor return our mocked instances + mock_storage_cls.side_effect = [ + mock_storage1, + mock_storage2, + ] + + members = expanded_collection.get_members() + + # Verify it created Storage objects from expanded data + self.assertEqual(2, len(members)) + self.assertEqual([mock_storage1, mock_storage2], members) + + # Check that Storage was initialized with expanded JSON data + self.assertEqual(2, mock_storage_cls.call_count) + + # Verify first member initialization with expanded data + first_call = mock_storage_cls.call_args_list[0] + self.assertEqual(expanded_collection._conn, first_call[0][0]) + self.assertEqual( + "/redfish/v1/Systems/1/Storage/RAID-1", first_call[0][1] + ) + # Check that expanded data was passed + self.assertEqual( + expanded_json["Members"][0], first_call[1]["json_doc"] + ) + + # Verify second member initialization with expanded data + second_call = mock_storage_cls.call_args_list[1] + self.assertEqual(expanded_collection._conn, second_call[0][0]) + self.assertEqual( + "/redfish/v1/Systems/1/Storage/NVMe-1", second_call[0][1] + ) + # Check that expanded data was passed + self.assertEqual( + expanded_json["Members"][1], second_call[1]["json_doc"] + ) + + def test_get_members_unexpanded(self): + # Test with unexpanded JSON data containing only references + # This simulates a normal response without ?$expand parameter + with open('sushy/tests/unit/json_samples/' + 'storage_collection.json') as f: + unexpanded_json = json.loads(f.read()) + + # Create collection without expand in URL + unexpanded_collection = storage.StorageCollection( + self.conn, + "/redfish/v1/Systems/1/Storage", + redfish_version="1.0.2", + ) + unexpanded_collection._json = unexpanded_json + + # Mock the parent get_members method which will fetch each member + # individually + mock_storage1 = mock.Mock(spec=storage.Storage) + mock_storage2 = mock.Mock(spec=storage.Storage) + with mock.patch.object( + storage.base.ResourceCollectionBase, + "get_members", + autospec=True, + return_value=[mock_storage1, mock_storage2], + ) as mock_super: + members = unexpanded_collection.get_members() + + # Should fall back to parent implementation for individual fetches + mock_super.assert_called_once_with(unexpanded_collection) + self.assertEqual([mock_storage1, mock_storage2], members) + + def test_get_members_no_json(self): + # Test when collection has no _json attribute + collection_no_json = storage.StorageCollection( + self.conn, + "/redfish/v1/Systems/437XR1138R2/Storage", + redfish_version="1.0.2", + ) + + # Mock the parent get_members method + with mock.patch.object( + storage.base.ResourceCollectionBase, + "get_members", + autospec=True, + return_value=["mock_member"], + ) as mock_super: + members = collection_no_json.get_members() + + # Should fall back to parent implementation + mock_super.assert_called_once() + self.assertEqual(["mock_member"], members) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/resources/system/test_port.py new/sushy-5.10.0/sushy/tests/unit/resources/system/test_port.py --- old/sushy-5.8.0/sushy/tests/unit/resources/system/test_port.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/resources/system/test_port.py 2026-02-23 11:14:12.000000000 +0100 @@ -53,6 +53,80 @@ self.port.ethernet.flow_control_status) self.assertEqual(net_cons.PortLinkStatus.LINKUP, self.port.link_status) + def test_lldp_receive_all_fields(self): + """Test all enhanced LLDP fields are parsed correctly""" + self.port._parse_attributes(self.json_doc) + + lldp = self.port.ethernet.lldp_receive + + # Test TLV Type 1 - Chassis ID with subtype + self.assertEqual('c4:7e:e0:e4:55:3f', lldp.chassis_id) + self.assertEqual(net_cons.IEEE802IdSubtype.MAC_ADDR, + lldp.chassis_id_subtype) + + # Test TLV Type 2 - Port ID with subtype + self.assertEqual('0A:1B:2C:3D:4E:5F:6A:7B:8C:9D:0E:1F:2A', + lldp.port_id) + self.assertEqual(net_cons.IEEE802IdSubtype.IF_NAME, + lldp.port_id_subtype) + + # Test TLV Type 5 - System Name + self.assertEqual('switch-00.example.com', lldp.system_name) + + # Test TLV Type 6 - System Description + self.assertEqual('Test Software, Version 00.00.00', + lldp.system_description) + + # Test TLV Type 7 - System Capabilities + self.assertIsNotNone(lldp.system_capabilities) + self.assertEqual(2, len(lldp.system_capabilities)) + self.assertIn(net_cons.LLDPSystemCapabilities.BRIDGE, + lldp.system_capabilities) + self.assertIn(net_cons.LLDPSystemCapabilities.ROUTER, + lldp.system_capabilities) + + # Test TLV Type 8 - Management Addresses + self.assertEqual('192.168.1.1', lldp.management_address_ipv4) + self.assertEqual('fe80::1', lldp.management_address_ipv6) + self.assertEqual('c4:7e:e0:e4:55:40', lldp.management_address_mac) + self.assertEqual(100, lldp.management_vlan_id) + + def test_lldp_receive_minimal_data(self): + """Test LLDP with minimal data (only mandatory fields)""" + minimal_doc = self.json_doc.copy() + minimal_doc['Ethernet']['LLDPReceive'] = { + "ChassisId": "aa:bb:cc:dd:ee:ff", + "PortId": "port-1" + } + + self.port._parse_attributes(minimal_doc) + lldp = self.port.ethernet.lldp_receive + + # Mandatory fields present + self.assertEqual('aa:bb:cc:dd:ee:ff', lldp.chassis_id) + self.assertEqual('port-1', lldp.port_id) + + # Optional fields None + self.assertIsNone(lldp.chassis_id_subtype) + self.assertIsNone(lldp.port_id_subtype) + self.assertIsNone(lldp.system_name) + self.assertIsNone(lldp.system_description) + self.assertIsNone(lldp.system_capabilities) + self.assertIsNone(lldp.management_address_ipv4) + + def test_lldp_receive_empty(self): + """Test empty LLDPReceive (Dell scenario)""" + empty_doc = self.json_doc.copy() + empty_doc['Ethernet']['LLDPReceive'] = {} + + self.port._parse_attributes(empty_doc) + lldp = self.port.ethernet.lldp_receive + + # All fields should be None + self.assertIsNone(lldp.chassis_id) + self.assertIsNone(lldp.port_id) + self.assertIsNone(lldp.system_name) + class PortCollectionTestCase(base.TestCase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/resources/system/test_simple_storage.py new/sushy-5.10.0/sushy/tests/unit/resources/system/test_simple_storage.py --- old/sushy-5.8.0/sushy/tests/unit/resources/system/test_simple_storage.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/resources/system/test_simple_storage.py 2026-02-23 11:14:12.000000000 +0100 @@ -141,3 +141,118 @@ self.conn.get.return_value.json.return_value = json.load(f) self.assertEqual(8000000000000, self.simpl_stor_col.max_size_bytes) + + def test_get_members_expanded(self): + # Test with expanded JSON data containing full member objects + # This simulates a response from ?$expand=.($levels=1) + with open('sushy/tests/unit/json_samples/' + 'simple_storage_collection_expanded.json') as f: + expanded_json = json.loads(f.read()) + + # Create collection with the URL that would have ?$expand appended + expanded_collection = simple_storage.SimpleStorageCollection( + self.conn, + "/redfish/v1/Systems/1/SimpleStorage?$expand=.($levels=1)", + redfish_version="1.0.2", + ) + expanded_collection._json = expanded_json + + # Mock the SimpleStorage objects that will be created + mock_member1 = mock.Mock(spec=simple_storage.SimpleStorage) + mock_member1.identity = "RAID-1" + mock_member1.name = "RAID Controller" + + mock_member2 = mock.Mock(spec=simple_storage.SimpleStorage) + mock_member2.identity = "AHCI-1" + mock_member2.name = "AHCI Controller" + + with mock.patch.object( + simple_storage, "SimpleStorage", autospec=True + ) as mock_simple_storage: + # Make the constructor return our mocked instances + mock_simple_storage.side_effect = [mock_member1, mock_member2] + + members = expanded_collection.get_members() + + # Verify it created SimpleStorage objects from expanded data + self.assertEqual(2, len(members)) + self.assertEqual([mock_member1, mock_member2], members) + + # Check that SimpleStorage was initialized with expanded JSON data + self.assertEqual(2, mock_simple_storage.call_count) + + # Verify first member initialization with expanded data + first_call = mock_simple_storage.call_args_list[0] + self.assertEqual(expanded_collection._conn, first_call[0][0]) + self.assertEqual( + "/redfish/v1/Systems/1/SimpleStorage/RAID-1", + first_call[0][1], + ) + # Check that expanded data was passed + self.assertEqual( + expanded_json["Members"][0], first_call[1]["json_doc"] + ) + + # Verify second member initialization with expanded data + second_call = mock_simple_storage.call_args_list[1] + self.assertEqual(expanded_collection._conn, second_call[0][0]) + self.assertEqual( + "/redfish/v1/Systems/1/SimpleStorage/AHCI-1", + second_call[0][1], + ) + # Check that expanded data was passed + self.assertEqual( + expanded_json["Members"][1], second_call[1]["json_doc"] + ) + + def test_get_members_unexpanded(self): + # Test with unexpanded JSON data containing only references + # This simulates a normal response without ?$expand parameter + with open('sushy/tests/unit/json_samples/' + 'simple_storage_collection.json') as f: + unexpanded_json = json.loads(f.read()) + + # Create collection without expand in URL + unexpanded_collection = simple_storage.SimpleStorageCollection( + self.conn, + "/redfish/v1/Systems/1/SimpleStorage", + redfish_version="1.0.2", + ) + unexpanded_collection._json = unexpanded_json + + # Mock the parent get_members method which will fetch each member + # individually + mock_member1 = mock.Mock(spec=simple_storage.SimpleStorage) + mock_member2 = mock.Mock(spec=simple_storage.SimpleStorage) + with mock.patch.object( + simple_storage.base.ResourceCollectionBase, + "get_members", + autospec=True, + return_value=[mock_member1, mock_member2], + ) as mock_super: + members = unexpanded_collection.get_members() + + # Should fall back to parent implementation for individual fetches + mock_super.assert_called_once_with(unexpanded_collection) + self.assertEqual([mock_member1, mock_member2], members) + + def test_get_members_no_json(self): + # Test when collection has no _json attribute + collection_no_json = simple_storage.SimpleStorageCollection( + self.conn, + "/redfish/v1/Systems/1/SimpleStorage", + redfish_version="1.0.2", + ) + + # Mock the parent get_members method + with mock.patch.object( + simple_storage.base.ResourceCollectionBase, + "get_members", + autospec=True, + return_value=["mock_member"], + ) as mock_super: + members = collection_no_json.get_members() + + # Should fall back to parent implementation + mock_super.assert_called_once() + self.assertEqual(["mock_member"], members) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/resources/system/test_system.py new/sushy-5.10.0/sushy/tests/unit/resources/system/test_system.py --- old/sushy-5.8.0/sushy/tests/unit/resources/system/test_system.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/resources/system/test_system.py 2026-02-23 11:14:12.000000000 +0100 @@ -129,18 +129,6 @@ 'ComputerSystem.Reset'}}, attributes.get('_actions')) - def test__parse_attributes_missing_actions(self): - self.sys_inst.json.pop('Actions') - self.assertRaisesRegex( - exceptions.MissingAttributeError, 'attribute Actions', - self.sys_inst._parse_attributes, self.json_doc) - - def test__parse_attributes_missing_boot(self): - self.sys_inst.json.pop('Boot') - self.assertRaisesRegex( - exceptions.MissingAttributeError, 'attribute Boot', - self.sys_inst._parse_attributes, self.json_doc) - def test__parse_attributes_missing_reset_target(self): self.sys_inst.json['Actions']['#ComputerSystem.Reset'].pop( 'target') @@ -872,11 +860,11 @@ self.assertIsInstance(self.sys_inst.simple_storage, simple_storage.SimpleStorageCollection) - def test_storage_for_missing_attr(self): + def test_simplestorage_for_missing_attr(self): self.sys_inst.json.pop('SimpleStorage') with self.assertRaisesRegex( - exceptions.MissingAttributeError, 'attribute Storage'): - self.sys_inst.storage + exceptions.MissingAttributeError, 'attribute SimpleStorage'): + self.sys_inst.simple_storage def test_storage(self): # | GIVEN | @@ -973,6 +961,24 @@ self.assertEqual( '/redfish/v1/Chassis/1U', actual_chassis[0].path) + @mock.patch.object(chassis, 'Chassis', autospec=True) + def test_chassis_expanded(self, mock_chassis): + # Test accessing expanded chassis property + result = self.sys_inst.chassis_expanded + mock_chassis.assert_called_once_with( + self.conn, '/redfish/v1/Chassis/1U?$expand=.($levels=1)', + redfish_version=self.sys_inst.redfish_version, + registries=self.sys_inst.registries, root=self.sys_inst.root) + self.assertEqual([mock_chassis.return_value], result) + + def test_chassis_missing_attr(self): + # Test missing chassis attribute + self.sys_inst._json['Links'].pop('Chassis') + self.assertRaisesRegex( + exceptions.MissingAttributeError, + 'attribute Links/Chassis', + lambda: self.sys_inst.chassis) + def test_get_oem_extension(self): # | WHEN | contoso_system_extn_inst = self.sys_inst.get_oem_extension('Contoso') @@ -1218,6 +1224,39 @@ self.assertIsNone(boot_field_instance.mode) self.assertIsNone(boot_field_instance.target) + def test_storage_for_missing_attr(self): + self.sys_inst.json.pop('Storage') + with self.assertRaisesRegex( + exceptions.MissingAttributeError, 'attribute Storage'): + self.sys_inst.storage + + @mock.patch('sushy.resources.system.storage.storage.StorageCollection', + autospec=True) + def test_storage_expanded(self, mock_storage_collection): + # Test accessing expanded storage property + result = self.sys_inst.storage_expanded + mock_storage_collection.assert_called_once_with( + self.conn, + '/redfish/v1/Systems/437XR1138R2/Storage?$expand=.($levels=1)', + redfish_version='1.0.2', + registries=None, + root=None) + self.assertIsNotNone(result) + + @mock.patch('sushy.resources.system.simple_storage.' + 'SimpleStorageCollection', autospec=True) + def test_simple_storage_expanded(self, mock_simple_storage_collection): + # Test accessing expanded simple_storage property + result = self.sys_inst.simple_storage_expanded + mock_simple_storage_collection.assert_called_once_with( + self.conn, + '/redfish/v1/Systems/437XR1138R2/SimpleStorage?' + '$expand=.($levels=1)', + redfish_version='1.0.2', + registries=None, + root=None) + self.assertIsNotNone(result) + class SystemWithVirtualMedia(base.TestCase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/test_connector.py new/sushy-5.10.0/sushy/tests/unit/test_connector.py --- old/sushy-5.8.0/sushy/tests/unit/test_connector.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/test_connector.py 2026-02-23 11:14:12.000000000 +0100 @@ -195,6 +195,14 @@ 'GET', 'http://foo.bar:1234/fake/path', headers=self.headers, json=None, verify=True, timeout=42) + def test_ok_get_connect_timeout_tuple(self): + self.conn._default_request_timeout = 60 + self.conn._connect_timeout = 10 + self.conn._op('GET', path='fake/path') + self.request.assert_called_once_with( + 'GET', 'http://foo.bar:1234/fake/path', + headers=self.headers, json=None, verify=True, timeout=(10, 60)) + def test_response_callback(self): mock_response_callback = mock.MagicMock() self.conn._response_callback = mock_response_callback @@ -701,6 +709,32 @@ with self.assertRaisesRegex(exceptions.BadRequestError, message): self.conn._op('POST', 'http://foo.bar', blocking=True) + @mock.patch('sushy.connector.time.sleep', autospec=True) + def test_blocking_with_connect_timeout_tuple(self, mock_sleep): + # Test that TaskMonitor.wait() receives the read timeout (second value) + # when timeout is a tuple (connect, read). This verifies that the + # tuple doesn't cause a TypeError in TaskMonitor.wait(). + self.conn._connect_timeout = 10 + self.conn._default_request_timeout = 60 + response1 = mock.MagicMock(spec=requests.Response) + response1.status_code = http_client.ACCEPTED + response1.headers = { + 'Retry-After': 5, + 'Location': '/redfish/v1/taskmon/1', + 'Content-Length': 10 + } + response1.json.return_value = {'Id': 3, 'Name': 'Test'} + response2 = mock.MagicMock(spec=requests.Response) + response2.status_code = http_client.OK + response2.json.return_value = {} + self.request.side_effect = [response1, response2] + # This should not raise - the tuple timeout should be handled correctly + # by extracting the read timeout for TaskMonitor.wait() + self.conn._op('POST', 'http://foo.bar', blocking=True) + # Verify the first request was called with the tuple timeout + first_call = self.request.call_args_list[0] + self.assertEqual(first_call[1]['timeout'], (10, 60)) + @mock.patch.object(connector.LOG, 'warning', autospec=True) def test_access_error_basic_auth(self, mock_log): self.conn._auth.can_refresh_session.return_value = False diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy/tests/unit/test_main.py new/sushy-5.10.0/sushy/tests/unit/test_main.py --- old/sushy-5.8.0/sushy/tests/unit/test_main.py 2025-11-13 14:54:46.000000000 +0100 +++ new/sushy-5.10.0/sushy/tests/unit/test_main.py 2026-02-23 11:14:12.000000000 +0100 @@ -54,7 +54,8 @@ verify=True, auth=mock_auth) mock_connector.assert_called_once_with( 'http://foo.bar:1234', verify=True, server_side_retries=10, - server_side_retries_delay=3) + server_side_retries_delay=3, default_request_timeout=60, + connect_timeout=None) def test__parse_attributes(self): self.root._parse_attributes(self.json_doc) @@ -87,6 +88,32 @@ ValueError, main.Sushy, 'http://foo.bar:1234', 'foo', 'bar', auth=mock.MagicMock()) + @mock.patch.object(auth, 'SessionOrBasicAuth', autospec=True) + @mock.patch.object(connector, 'Connector', autospec=True) + def test_custom_read_timeout(self, mock_connector, mock_auth): + mock_connector.return_value = self.conn + with open('sushy/tests/unit/json_samples/root.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + main.Sushy('http://foo.bar:1234', verify=True, auth=mock_auth, + read_timeout=30) + mock_connector.assert_called_once_with( + 'http://foo.bar:1234', verify=True, server_side_retries=10, + server_side_retries_delay=3, default_request_timeout=30, + connect_timeout=None) + + @mock.patch.object(auth, 'SessionOrBasicAuth', autospec=True) + @mock.patch.object(connector, 'Connector', autospec=True) + def test_custom_connect_timeout(self, mock_connector, mock_auth): + mock_connector.return_value = self.conn + with open('sushy/tests/unit/json_samples/root.json') as f: + self.conn.get.return_value.json.return_value = json.load(f) + main.Sushy('http://foo.bar:1234', verify=True, auth=mock_auth, + read_timeout=60, connect_timeout=10) + mock_connector.assert_called_once_with( + 'http://foo.bar:1234', verify=True, server_side_retries=10, + server_side_retries_delay=3, default_request_timeout=60, + connect_timeout=10) + @mock.patch.object(connector, 'Connector', autospec=True) def test_custom_connector(self, mock_Sushy_Connector): connector_mock = mock.MagicMock() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy.egg-info/PKG-INFO new/sushy-5.10.0/sushy.egg-info/PKG-INFO --- old/sushy-5.8.0/sushy.egg-info/PKG-INFO 2025-11-13 14:55:50.000000000 +0100 +++ new/sushy-5.10.0/sushy.egg-info/PKG-INFO 2026-02-23 11:14:54.000000000 +0100 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: sushy -Version: 5.8.0 +Version: 5.10.0 Summary: Sushy is a small Python library to communicate with Redfish based systems Home-page: https://docs.openstack.org/sushy/latest/ Author: OpenStack @@ -23,6 +23,15 @@ Requires-Dist: requests>=2.14.2 Requires-Dist: python-dateutil>=2.7.0 Requires-Dist: stevedore>=1.29.0 +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license-file +Dynamic: requires-dist +Dynamic: requires-python +Dynamic: summary Overview ======== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy.egg-info/SOURCES.txt new/sushy-5.10.0/sushy.egg-info/SOURCES.txt --- old/sushy-5.8.0/sushy.egg-info/SOURCES.txt 2025-11-13 14:55:50.000000000 +0100 +++ new/sushy-5.10.0/sushy.egg-info/SOURCES.txt 2026-02-23 11:14:54.000000000 +0100 @@ -31,6 +31,7 @@ releasenotes/notes/add-bios-update-status-cc59816c374b78e4.yaml releasenotes/notes/add-chassis-linkage-d8e567f9c791169d.yaml releasenotes/notes/add-chassis-support-5b97daffe1c61a2b.yaml +releasenotes/notes/add-connection-timeout-param-d6c403e7809267bc.yaml releasenotes/notes/add-custom-connector-support-0a49c6649d5f7eaf.yaml releasenotes/notes/add-default-identity-10c5dd23bed0e915.yaml releasenotes/notes/add-drive-led-97b687013fec88c9.yaml @@ -40,6 +41,7 @@ releasenotes/notes/add-fabric-support-1520f7fcb0e12539.yaml releasenotes/notes/add-http-boot-uri-support-5c25816e13ccdb27.yaml releasenotes/notes/add-initial-redfish-oem-extension-support-50c9849bb7b6b25c.yaml +releasenotes/notes/add-lldp-receive-fields-enhancement-f1e3d4a5b2c6d789.yaml releasenotes/notes/add-mapped-list-field-04c671f7a73d83f6.yaml releasenotes/notes/add-network-adapter-26d01d8d9fb1d7ad.yaml releasenotes/notes/add-network-device-function-and-port-e880d8f461e3723d.yaml @@ -86,6 +88,7 @@ releasenotes/notes/deprecate-system-leds-f1a72422c53d281e.yaml releasenotes/notes/disable-conn-pooling-3456782afe56ac94.yaml releasenotes/notes/do-not-offer-compression-encoding-884ca8a7458cb096.yaml +releasenotes/notes/do_not_require_actions_or_boot-b495d5407666be8b.yaml releasenotes/notes/drop-py-2-7-cc931c210ce08e33.yaml releasenotes/notes/enhance-oem-extension-design-3143717e710b3eaf.yaml releasenotes/notes/enhance-storage-volume-drive-support-16314d30f3631fb3.yaml @@ -121,6 +124,7 @@ releasenotes/notes/fixes-ilo5-redfish-firmware-update-issue-273862b2a11e3536.yaml releasenotes/notes/get-retry-9ca311caf8a0b7bb.yaml releasenotes/notes/handle-basic-auth-access-errors-393b368b31f5cad2.yaml +releasenotes/notes/handle-missing-collection-members-8ccbd4588790eabe.yaml releasenotes/notes/handle_transfer_method-a51d5a17e381ebee.yaml releasenotes/notes/health_literals_change-0e3fc0c439b765e3.yaml releasenotes/notes/idrac10-settings-resource-c18e754c661a7056.yaml @@ -442,9 +446,12 @@ sushy/tests/unit/json_samples/settings.json sushy/tests/unit/json_samples/simple_storage.json sushy/tests/unit/json_samples/simple_storage_collection.json +sushy/tests/unit/json_samples/simple_storage_collection_expanded.json sushy/tests/unit/json_samples/softwareinventory.json sushy/tests/unit/json_samples/storage.json sushy/tests/unit/json_samples/storage_collection.json +sushy/tests/unit/json_samples/storage_collection_expanded.json +sushy/tests/unit/json_samples/storage_collection_multiple.json sushy/tests/unit/json_samples/storage_controller.json sushy/tests/unit/json_samples/storage_controller_collection.json sushy/tests/unit/json_samples/storage_controller_settings.json diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/sushy-5.8.0/sushy.egg-info/pbr.json new/sushy-5.10.0/sushy.egg-info/pbr.json --- old/sushy-5.8.0/sushy.egg-info/pbr.json 2025-11-13 14:55:50.000000000 +0100 +++ new/sushy-5.10.0/sushy.egg-info/pbr.json 2026-02-23 11:14:54.000000000 +0100 @@ -1 +1 @@ -{"git_version": "1e7fea1", "is_release": true} \ No newline at end of file +{"git_version": "4db9fc0", "is_release": true} \ No newline at end of file
