Skip to content

Commit 4163365

Browse files
authored
Fetch firmware versions for Cmis Target FW upgrade supported cables (#455)
* Fetch firmware versions for Cmis Target FW upgrade supported cables Signed-off-by: Mihir Patel <[email protected]> * Enhanced unit-test for exception handling while reading target firmware version * Addressed PR comments and moved server firmware version info to xcvr_field --------- Signed-off-by: Mihir Patel <[email protected]>
1 parent df6b6c6 commit 4163365

File tree

7 files changed

+251
-3
lines changed

7 files changed

+251
-3
lines changed

sonic_platform_base/sonic_xcvr/api/public/cmisTargetFWUpgrade.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,132 @@
55
upgrade of remote target from the local target itself.
66
"""
77

8+
import struct
9+
import sys
10+
import traceback
811
from ...fields import consts
912
from .cmis import CmisApi
1013

14+
import logging
15+
16+
logger = logging.getLogger(__name__)
17+
logger.addHandler(logging.NullHandler())
18+
19+
TARGET_E0_VALUE = 0
20+
TARGET_E1_VALUE = 1
21+
TARGET_E2_VALUE = 2
22+
23+
SERVER_FW_VERSION_SIZE = 16
24+
SERVER_FW_VERSION_NUMBER_SIZE = 4
25+
26+
TARGET_LIST = [TARGET_E0_VALUE, TARGET_E1_VALUE, TARGET_E2_VALUE]
27+
28+
CABLE_E1_FIRMWARE_INFO_MAP = {
29+
'active_firmware': 'e1_active_firmware',
30+
'inactive_firmware': 'e1_inactive_firmware',
31+
'server_firmware': 'e1_server_firmware'
32+
}
33+
34+
CABLE_E2_FIRMWARE_INFO_MAP = {
35+
'active_firmware': 'e2_active_firmware',
36+
'inactive_firmware': 'e2_inactive_firmware',
37+
'server_firmware': 'e2_server_firmware'
38+
}
39+
40+
REMOTE_TARGET_FIRMWARE_INFO_MAP = {
41+
TARGET_E1_VALUE: CABLE_E1_FIRMWARE_INFO_MAP,
42+
TARGET_E2_VALUE: CABLE_E2_FIRMWARE_INFO_MAP,
43+
}
44+
1145
class CmisTargetFWUpgradeAPI(CmisApi):
1246
def set_firmware_download_target_end(self, target):
1347
return self.xcvr_eeprom.write(consts.TARGET_MODE, target)
48+
49+
"""
50+
Reads the active, inactive and server firmware version from all targets
51+
and returns a dictionary of the firmware versions.
52+
Returns:
53+
A dictionary of the firmware versions for all targets.
54+
"""
55+
def get_transceiver_info_firmware_versions(self):
56+
return_dict = {
57+
'active_firmware': 'N/A',
58+
'inactive_firmware': 'N/A',
59+
'e1_active_firmware': 'N/A',
60+
'e1_inactive_firmware': 'N/A',
61+
'e2_active_firmware': 'N/A',
62+
'e2_inactive_firmware': 'N/A',
63+
'e1_server_firmware': 'N/A',
64+
'e2_server_firmware': 'N/A'
65+
}
66+
67+
for target in TARGET_LIST:
68+
try:
69+
if not self.set_firmware_download_target_end(target):
70+
logging.error("Target mode change failed. Target: {}".format(target))
71+
continue
72+
73+
# Any register apart from the TARGET_MODE register will have the value 0xff
74+
# if the remote target is not accessible from the local target.
75+
module_type = self.get_module_type()
76+
if 'Unknown' in module_type:
77+
logging.info("Remote target {} not accessible. Skipping.".format(target))
78+
continue
79+
80+
firmware_versions = super().get_transceiver_info_firmware_versions()
81+
if target in REMOTE_TARGET_FIRMWARE_INFO_MAP:
82+
# Add server firmware version to the firmware_versions dictionary
83+
firmware_versions.update(self._get_server_firmware_version())
84+
return_dict.update(self._convert_firmware_info_to_target_firmware_info(
85+
firmware_versions, REMOTE_TARGET_FIRMWARE_INFO_MAP[target]))
86+
else:
87+
return_dict.update(firmware_versions)
88+
except Exception as e:
89+
logging.error("Exception occurred while handling target {} firmware version: {}".format(target, repr(e)))
90+
exc_type, exc_value, exc_traceback = sys.exc_info()
91+
msg = traceback.format_exception(exc_type, exc_value, exc_traceback)
92+
for tb_line in msg:
93+
for tb_line_split in tb_line.splitlines():
94+
logging.error(tb_line_split)
95+
continue
96+
97+
self.set_firmware_download_target_end(TARGET_E0_VALUE)
98+
return return_dict
99+
100+
def _convert_firmware_info_to_target_firmware_info(self, firmware_info, firmware_info_map):
101+
return_dict = {}
102+
for key, value in firmware_info_map.items():
103+
if key in firmware_info:
104+
return_dict[value] = firmware_info[key]
105+
return return_dict
106+
107+
"""
108+
Reads the server firmware version and return a dictionary of the server firmware version.
109+
The server firmware version is of the format "A.B.C.D" where A, B, C and D are 4 bytes each representing a number.
110+
Following are the steps to read the server firmware version:
111+
1. Read the magic byte at page 3h, offset 128. If this has the value 0x0, then the server
112+
firmware version is not available and hence, return without proceeding to step 2.
113+
2. Calculate the checksum of the server firmware version. If the checksum is not valid, then the server
114+
firmware version is not available. If the checksum is valid, then proceed to step 3.
115+
3. Read the server firmware version from page 3h, offset 130-145.
116+
Returns:
117+
A dictionary of the server firmware version.
118+
"""
119+
def _get_server_firmware_version(self):
120+
return_dict = {
121+
'server_firmware': 'N/A'
122+
}
123+
124+
magic_byte = self.xcvr_eeprom.read(consts.SERVER_FW_MAGIC_BYTE)
125+
if magic_byte != 0:
126+
checksum = self.xcvr_eeprom.read(consts.SERVER_FW_CHECKSUM)
127+
server_fw_version_byte_array, server_fw_version_str = self.xcvr_eeprom.read(consts.SERVER_FW_VERSION)
128+
129+
calculated_checksum = 0
130+
for byte in server_fw_version_byte_array:
131+
calculated_checksum += byte
132+
133+
if calculated_checksum & 0xFF == checksum:
134+
return_dict['server_firmware'] = server_fw_version_str
135+
136+
return return_dict

sonic_platform_base/sonic_xcvr/api/xcvr_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ def get_transceiver_info_firmware_versions(self):
7575
Retrieves active and inactive firmware versions of the xcvr
7676
7777
Returns:
78-
A list with active and inactive firmware versions of the xcvr
79-
[active_firmware, inactive_firmware]
78+
A dictionary containing the active and inactive firmware versions of the transceiver
79+
{'active_firmware' : 'A.B.C', 'inactive_firmware' : 'X.Y.Z'}
8080
"""
8181
raise NotImplementedError
8282

sonic_platform_base/sonic_xcvr/fields/consts.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,12 @@
481481
CDB_CMD = "CdbCommand"
482482
CDB_WRITE_MSG = "CdbWriteMessage"
483483

484+
#CMISTargetFWUpgrade
485+
CMIS_TARGET_SERVER_INFO = "CmisTargetServerInfo"
486+
SERVER_FW_MAGIC_BYTE = "ServerFirmwareMagicByte"
487+
SERVER_FW_CHECKSUM = "ServerFirmwareChecksum"
488+
SERVER_FW_VERSION = "ServerFirmwareVersion"
489+
484490
#VENDOR SPECIFIC
485491
VENDOR_CUSTOM = "VendorCustom"
486492
TARGET_MODE = "TargetMode"

sonic_platform_base/sonic_xcvr/fields/xcvr_field.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,26 @@ def __init__(self, name, offset, *fields, **kwargs):
250250
def decode(self, raw_data, **decoded_deps):
251251
return '-'.join([ "%02x" % byte for byte in raw_data])
252252

253+
class ServerFWVersionRegField(RegField):
254+
"""
255+
Returns the raw byte(s)
256+
"""
257+
def __init__(self, name, offset, *fields, **kwargs):
258+
super(ServerFWVersionRegField, self).__init__(name, offset, *fields, **kwargs)
259+
260+
def decode(self, raw_data, **decoded_deps):
261+
server_fw_version_str = ''
262+
server_fw_version_size = 16
263+
server_fw_version_number_size = 4
264+
265+
# Use a list comprehension to convert each 4-byte number to a string
266+
server_fw_version_str = '.'.join(
267+
str(struct.unpack('>I', raw_data[i:i+server_fw_version_number_size])[0])
268+
for i in range(0, server_fw_version_size, server_fw_version_number_size)
269+
)
270+
271+
return raw_data, server_fw_version_str
272+
253273
class RegGroupField(XcvrField):
254274
"""
255275
Field denoting one or more bytes, logically interpreted as one or more RegFields

sonic_platform_base/sonic_xcvr/mem_maps/public/cmisTargetFWUpgrade.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,19 @@
66
"""
77

88
from .cmis import CmisMemMap
9+
from ...fields.xcvr_field import (
10+
NumberRegField,
11+
RegGroupField,
12+
ServerFWVersionRegField
13+
)
14+
from ...fields import consts
915

1016
class CmisTargetFWUpgradeMemMap(CmisMemMap):
1117
# Vendor agnostic implementation to be added here
12-
pass
18+
def __init__(self, codes):
19+
super().__init__(codes)
20+
21+
self.CMIS_TARGET_SERVER_INFO = RegGroupField(consts.CMIS_TARGET_SERVER_INFO,
22+
NumberRegField(consts.SERVER_FW_MAGIC_BYTE, self.getaddr(0x3, 128), format="B", size=1),
23+
NumberRegField(consts.SERVER_FW_CHECKSUM, self.getaddr(0x3, 129), format="B", size=1),
24+
ServerFWVersionRegField(consts.SERVER_FW_VERSION, self.getaddr(0x3, 130), size=16))
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from unittest.mock import patch
2+
from mock import MagicMock
3+
import pytest
4+
from sonic_platform_base.sonic_xcvr.api.public.cmisTargetFWUpgrade import TARGET_E0_VALUE, TARGET_LIST, CmisTargetFWUpgradeAPI
5+
from sonic_platform_base.sonic_xcvr.codes.public.cmisTargetFWUpgrade import CmisTargetFWUpgradeCodes
6+
from sonic_platform_base.sonic_xcvr.mem_maps.public.cmisTargetFWUpgrade import CmisTargetFWUpgradeMemMap
7+
from sonic_platform_base.sonic_xcvr.xcvr_eeprom import XcvrEeprom
8+
9+
class TestCmis(object):
10+
codes = CmisTargetFWUpgradeCodes
11+
mem_map = CmisTargetFWUpgradeMemMap(codes)
12+
reader = MagicMock(return_value=None)
13+
writer = MagicMock()
14+
eeprom = XcvrEeprom(reader, writer, mem_map)
15+
api = CmisTargetFWUpgradeAPI(eeprom)
16+
17+
@pytest.mark.parametrize("set_firmware_result, module_type, exception_raised", [
18+
(False, 'QSFP+ or later with CMIS', False),
19+
(True, 'Unknown', False),
20+
(True, 'QSFP+ or later with CMIS', True)
21+
])
22+
@patch('sonic_platform_base.sonic_xcvr.api.public.cmis.CmisApi.get_transceiver_info_firmware_versions', MagicMock(side_effect=({}, Exception('error'), {})))
23+
@patch('sonic_platform_base.sonic_xcvr.api.public.cmisTargetFWUpgrade.CmisTargetFWUpgradeAPI._get_server_firmware_version', MagicMock())
24+
@patch('traceback.format_exception')
25+
def test_get_transceiver_info_firmware_versions_failure(self, mock_format_exception, set_firmware_result, module_type, exception_raised):
26+
expected_output = {'active_firmware': 'N/A', 'inactive_firmware': 'N/A', 'e1_active_firmware': 'N/A',\
27+
'e1_inactive_firmware': 'N/A', 'e2_active_firmware': 'N/A', 'e2_inactive_firmware': 'N/A',\
28+
'e1_server_firmware': 'N/A', 'e2_server_firmware': 'N/A'}
29+
self.api.set_firmware_download_target_end = MagicMock(return_value=set_firmware_result)
30+
self.api.get_module_type = MagicMock(return_value=module_type)
31+
32+
result = self.api.get_transceiver_info_firmware_versions()
33+
assert result == expected_output
34+
35+
assert self.api.set_firmware_download_target_end.call_count == len(TARGET_LIST) + 1
36+
# Ensure that FW version is read for all targets
37+
for index, call in enumerate(self.api.set_firmware_download_target_end.call_args_list):
38+
args, _ = call
39+
# Ensure target is restore to E0 after reading FW version from all targets
40+
if index == len(TARGET_LIST):
41+
assert args[0] == TARGET_E0_VALUE
42+
else:
43+
assert args[0] == TARGET_LIST[index]
44+
45+
if exception_raised:
46+
assert mock_format_exception.call_count == 1
47+
assert self.api._get_server_firmware_version.call_count == 1
48+
else:
49+
self.api._get_server_firmware_version.assert_not_called()
50+
51+
@pytest.mark.parametrize("fw_info_dict, server_fw_info_dict, expected_output", [
52+
(({'active_firmware': '1.1.1', 'inactive_firmware': '1.0.0'}, {'active_firmware': '1.1.1', 'inactive_firmware': '1.0.0'}, {'active_firmware': '1.1.1', 'inactive_firmware': '1.0.0'}), ({'server_firmware': '1.5.0.1421'}, {'server_firmware': '1.5.0.1421'}),\
53+
{'active_firmware': '1.1.1', 'inactive_firmware': '1.0.0', 'e1_active_firmware': '1.1.1', 'e1_inactive_firmware': '1.0.0', 'e2_active_firmware': '1.1.1', 'e2_inactive_firmware': '1.0.0', 'e1_server_firmware': '1.5.0.1421', 'e2_server_firmware': '1.5.0.1421'}),
54+
(({'active_firmware': '1.1.1', 'inactive_firmware': '1.0.0'}, {'active_firmware': '2.1.1', 'inactive_firmware': '1.0.0'}, {'active_firmware': '1.1.1', 'inactive_firmware': '2.0.1'}), ({'server_firmware': '1223.6.0.739'}, {'server_firmware': '93.5.0.3431'}),\
55+
{'active_firmware': '1.1.1', 'inactive_firmware': '1.0.0', 'e1_active_firmware': '2.1.1', 'e1_inactive_firmware': '1.0.0', 'e2_active_firmware': '1.1.1', 'e2_inactive_firmware': '2.0.1', 'e1_server_firmware': '1223.6.0.739', 'e2_server_firmware': '93.5.0.3431'})
56+
])
57+
def test_get_transceiver_info_firmware_versions_success(self, fw_info_dict, server_fw_info_dict, expected_output):
58+
with patch('sonic_platform_base.sonic_xcvr.api.public.cmis.CmisApi.get_transceiver_info_firmware_versions', side_effect=fw_info_dict):
59+
with patch('sonic_platform_base.sonic_xcvr.api.public.cmisTargetFWUpgrade.CmisTargetFWUpgradeAPI._get_server_firmware_version', side_effect=server_fw_info_dict):
60+
self.api.set_firmware_download_target_end = MagicMock(return_value=True)
61+
self.api.get_module_type = MagicMock(return_value='QSFP+ or later with CMIS')
62+
63+
result = self.api.get_transceiver_info_firmware_versions()
64+
assert result == expected_output
65+
assert self.api.set_firmware_download_target_end.call_count == len(TARGET_LIST) + 1
66+
67+
@pytest.mark.parametrize("magic_byte, checksum, server_fw_version_byte_array, expected", [
68+
(0, 0, (), {'server_firmware': 'N/A'}),
69+
(0, 0x98, [0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 5, 0x8d], {'server_firmware': 'N/A'}), # Magic byte is 0 but other values are valid
70+
(0xAC, 0x98, ([0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 5, 0x8d], "1.5.0.1421"), {'server_firmware': '1.5.0.1421'}),
71+
(0xff, 0xff, ([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], "N/A"), {'server_firmware': 'N/A'}),
72+
(0xAC, 0x98, ([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], "N/A"), {'server_firmware': 'N/A'})
73+
])
74+
def test_get_server_firmware_version(self, magic_byte, checksum, server_fw_version_byte_array, expected):
75+
self.api.xcvr_eeprom.read = MagicMock()
76+
self.api.xcvr_eeprom.read.side_effect = [magic_byte, checksum, server_fw_version_byte_array]
77+
78+
result = self.api._get_server_firmware_version()
79+
assert result == expected

tests/sonic_xcvr/test_xcvr_field.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
DateField,
44
FixedNumberRegField,
55
HexRegField,
6+
ServerFWVersionRegField,
67
NumberRegField,
78
RegBitField,
89
RegBitsField,
@@ -64,6 +65,7 @@ def __init__(self, codes):
6465
)
6566
self.STRING_REG = StringRegField("StringReg", 12, size=15)
6667
self.HEX_REG = HexRegField("HexReg", 30, size=3)
68+
self.BYTES_REG = ServerFWVersionRegField("BytesReg", 10, size=4)
6769
self.REG_GROUP = RegGroupField("RegGroup",
6870
NumberRegField("Field0", 6, ro=False),
6971
NumberRegField("Field1", 7,
@@ -298,6 +300,12 @@ def test_decode(self):
298300
data = bytearray([0xAA, 0xBB, 0xCC])
299301
assert field.decode(data) == "aa-bb-cc"
300302

303+
class TestServerFWVersionRegField(object):
304+
def test_decode(self):
305+
field = mem_map.get_field("BytesReg")
306+
data = bytearray([0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 5, 0x8d])
307+
assert field.decode(data) == (bytearray([0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 5, 0x8d]), "1.5.0.1421")
308+
301309
class TestRegGroupField(object):
302310
def test_offset(self):
303311
field = mem_map.get_field("RegGroup")

0 commit comments

Comments
 (0)