Skip to content
Open
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
39 changes: 34 additions & 5 deletions sonic_platform_base/sonic_xcvr/api/public/cdb_fw.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,35 @@ def _create_cdb_fw_handler(self):
self._init_cdb_fw_handler = False
return None

def _enter_password(self, password=cdb_consts.CDB_DEFAULT_PASSWORD):
"""
Enter the CMIS host password to unlock protected CDB/EEPROM access.

The password is entered via CDB command 0001h first, since that path is
synchronous (send_cmd waits for and checks the CDB status). If it fails
-- or CDB is not available at all -- fall back to the non-CDB Password
Entry Area write (the register is in the CMIS memory map, not the CDB
one).

Returns True if the password was accepted (or delivered best-effort),
False otherwise.
"""
if not isinstance(password, int) or \
password < 0 or password > 0xFFFFFFFF:
log.log_notice('Invalid password: must be an integer in range 0..0xFFFFFFFF')
return False

if self.cdb_fw_hdlr is None:
log.log_notice('CDB not available; entering password via Password Entry Area write')
return self.enter_password_via_memory(password) is True

if self.cdb_fw_hdlr.enter_password(password) is True:
return True

log.log_notice('CDB command 0001h password entry failed; '
'falling back to Password Entry Area write')
return self.enter_password_via_memory(password) is True

def get_module_fw_mgmt_feature(self, verbose = False):
"""
This function obtains CDB features supported by the module from CDB command 0041h,
Expand Down Expand Up @@ -105,7 +134,7 @@ def get_module_fw_info(self):
if fw_info is False or fw_info is None:
if self.get_status_code() == cdb_consts.CDB_PASSWORD_ERROR_STATUS:
log.log_notice('Get module FW info: Need to enter password')
self.cdb_fw_hdlr.enter_password()
self._enter_password()
fw_info = self.cdb_fw_hdlr.get_firmware_info()

if fw_info is False or fw_info is None:
Expand Down Expand Up @@ -238,7 +267,7 @@ def module_fw_run(self, mode = 0x01):
if fw_run_status == cdb_consts.CDB_PASSWORD_ERROR_STATUS:
string = 'Module FW run: Need to enter password\n'
log.log_notice(string)
self.cdb_fw_hdlr.enter_password()
self._enter_password()
result = self.cdb_fw_hdlr.run_fw_image(mode)
if result is not True:
txt += 'Module FW run: Fail after password retry\n'
Expand Down Expand Up @@ -276,7 +305,7 @@ def module_fw_commit(self):
if fw_commit_status == cdb_consts.CDB_PASSWORD_ERROR_STATUS:
string = 'Module FW commit: Need to enter password\n'
log.log_notice(string)
self.cdb_fw_hdlr.enter_password()
self._enter_password()
result = self.cdb_fw_hdlr.commit_fw_image()
if result is not True:
txt += 'Module FW commit: Fail after password retry\n'
Expand Down Expand Up @@ -339,7 +368,7 @@ def cdb_epl_block_write(self, address, data):
def cdb_enter_host_password(self, password):
if self.cdb_fw_hdlr is None:
return 0
if self.cdb_fw_hdlr.enter_password(password) is True:
if self._enter_password(password) is True:
log.log_notice('CDB host auth status: Success')
return 1
status = self.get_status_code()
Expand Down Expand Up @@ -373,7 +402,7 @@ def module_fw_start_download(self, imagepath):
# password error - retry with default password
if fw_start_status == cdb_consts.CDB_PASSWORD_ERROR_STATUS:
log.log_notice('Start module FW download: Need to enter password\n')
self.cdb_fw_hdlr.enter_password()
self._enter_password()
if self.cdb_fw_hdlr.start_fw_download(imagepath) is True:
return True, ''
txt = 'Start module FW download: Fail after password retry\n'
Expand Down
84 changes: 84 additions & 0 deletions sonic_platform_base/sonic_xcvr/api/public/cmis.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,90 @@ def get_cmis_rev(self):
cmis_rev = [str(num) for num in [cmis_major, cmis_minor]]
return '.'.join(cmis_rev)

def _supports_password_cmd_result(self):
"""
Whether the PasswordCmdResult register (00h:42.3-0) is defined for this
module. It was introduced in CMIS 5.3; on earlier modules those bits are
reserved, so their value must not be used to judge password acceptance.

Returns False if the CMIS revision cannot be determined, so the code
does not interpret a reserved register on a legacy module.
"""
try:
major, minor = (int(part) for part in self.get_cmis_rev().split("."))
except (AttributeError, ValueError):
return False
return (major, minor) >= consts.PASSWORD_RESULT_MIN_CMIS_REV

def _read_password_cmd_result(self):
"""
Poll PasswordCmdResult (00h:42.3-0) until validation completes.

Per CMIS 8.2.14, after a password entry/change WRITE the module updates
PasswordCmdResult within tWRITE, and until then may report "validation
in progress" or reject reads of the register. Poll past those transient
states, bounded by PASSWORD_RESULT_POLL_TIMEOUT.

Returns the 4-bit result code, or None if it could not be determined
within the timeout.
"""
elapsed = 0
while elapsed < consts.PASSWORD_RESULT_POLL_TIMEOUT:
result = self.xcvr_eeprom.read(consts.PASSWORD_CMD_RESULT)
if result is not None and \
result != consts.PASSWORD_RESULT_IN_PROGRESS:
return result
time.sleep(consts.PASSWORD_RESULT_POLL_INTERVAL / 1000)
elapsed += consts.PASSWORD_RESULT_POLL_INTERVAL
return None

def enter_password_via_memory(self, password):
"""
Enter the host password by writing the 4-byte value (MSB first) to the
Password Entry Area (page 00h bytes 122-125). This is the non-CDB
password entry method, used as a fallback for modules that unlock via
that register rather than CDB command 0001h.

Per CMIS 8.2.14 the write only delivers the password; its acceptance is
reported asynchronously in PasswordCmdResult (00h:42.3-0), which is only
defined on CMIS 5.3+ modules. So:
- On CMIS 5.3+, poll PasswordCmdResult and honor its verdict; a module
that explicitly rejects the password returns False.
- On earlier modules the register is reserved, so a successful write
is taken at face value (best-effort). The same best-effort result is
returned on 5.3+ when the register reports "not supported" or its
outcome cannot be determined.

Returns True if the password was accepted (or, best-effort, delivered),
False otherwise.
"""
# NumberRegField(format=">I") packs the password MSB-first for us.
if not self.xcvr_eeprom.write(consts.PASSWORD_ENTRY, password):
logger.warning("Password Entry Area write failed")
return False

# PasswordCmdResult (00h:42.3-0) only exists on CMIS 5.3+. On earlier
# modules there is no register to confirm acceptance, so treat the
# successful write as success.
if not self._supports_password_cmd_result():
return True

result = self._read_password_cmd_result()
if result in (consts.PASSWORD_RESULT_HOST_ACCEPTED,
consts.PASSWORD_RESULT_MODULE_ACCEPTED):
return True

if result == consts.PASSWORD_RESULT_NOT_ACCEPTED:
# Module honored the Password Entry Area and rejected the password.
logger.warning("Password not accepted by the module (PasswordCmdResult=0x3)")
return False

# 5.3+ module but the result is NOT_SUPPORTED or could not be determined:
# no register confirmation available, so treat the write as best-effort.
logger.warning("PasswordCmdResult unavailable (result={}); "
"treating Password Entry Area write as best-effort".format(result))
return True

# Transceiver status
def get_module_state(self):
'''
Expand Down
15 changes: 11 additions & 4 deletions sonic_platform_base/sonic_xcvr/cdb/cdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,22 @@ def get_cmd_status_code(self):

def enter_password(self, password=cdb_consts.CDB_DEFAULT_PASSWORD):
"""
Enter host password via CDB command 0001h.
Returns True if password accepted, False/None otherwise.
Enter the host password via CDB command 0001h. This path is synchronous:
send_cmd waits for and checks the CDB status.

Modules that do not unlock this way are handled by the caller, which
falls back to the non-CDB Password Entry Area write on the CmisApi.

Returns True if the password was accepted, False/None otherwise.
"""
if not isinstance(password, int) or password < 0 or password > 0xFFFFFFFF:
if not isinstance(password, int) or \
password < 0 or password > 0xFFFFFFFF:
log.log_notice("Invalid password: must be an integer in range 0..0xFFFFFFFF")
return False

payload = {"password": password}
return self.send_cmd(cdb_consts.CDB_ENTER_PASSWORD_CMD, payload)

def write_lpl_block(self, blkaddr, blkdata, timeout=None):
"""
Write LPL block
Expand Down
23 changes: 23 additions & 0 deletions sonic_platform_base/sonic_xcvr/fields/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,29 @@
HW_MINOR_REV = "ModuleHardwareMinorRevision"
CMIS_MAJOR_REVISION = "CmisMajorRevision"
CMIS_MINOR_REVISION = "CmisMinorRevision"

# CMIS Password Entry Area: page 00h bytes 122-125, 32-bit host password written
# MSB-first (big-endian). Standard, universally-supported way to unlock
# password-protected CDB/EEPROM access (per CMIS 8.2.14).
PASSWORD_ENTRY = "PasswordEntry"
PASSWORD_ENTRY_OFFSET = 122
PASSWORD_ENTRY_SIZE = 4
# PasswordCmdResult (00h:42.3-0): result of the most recent password entry/change
# written to the Password Entry Area. Writing the password only delivers it; its
# acceptance is reported asynchronously here. Only defined from CMIS 5.3 onward;
# on earlier modules those bits are reserved and must not be interpreted.
PASSWORD_CMD_RESULT = "PasswordCmdResult"
PASSWORD_RESULT_MIN_CMIS_REV = (5, 3)
PASSWORD_RESULT_NOT_SUPPORTED = 0x0 # not supported (legacy before CMIS 5.3)
PASSWORD_RESULT_MODULE_ACCEPTED = 0x1 # module password entry/change accepted
PASSWORD_RESULT_HOST_ACCEPTED = 0x2 # host password entry/change accepted
PASSWORD_RESULT_NOT_ACCEPTED = 0x3 # password entry not accepted
PASSWORD_RESULT_IN_PROGRESS = 0x8 # password validation in progress
# Poll bound for PasswordCmdResult after writing the Password Entry Area. The
# module updates the result within tWRITE and may reject reads (or report
# "in progress") until then; give it a small margin.
PASSWORD_RESULT_POLL_INTERVAL = 20 # msec
PASSWORD_RESULT_POLL_TIMEOUT = 1000 # msec
ACTIVE_FW_MAJOR_REV = "ModuleActiveFirmwareMajorRevision"
ACTIVE_FW_MINOR_REV = "ModuleActiveFirmwareMinorRevision"
INACTIVE_FW_MAJOR_REV = "ModuleInactiveFirmwareMajorRevision"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,18 @@ def __init__(self, codes, page=ADMINISTRATIVE_PAGE):
CodeRegField(consts.MODULE_FUNCTION_TYPE, self.getaddr(57), codes.MODULE_FUNCTION_TYPE),
]

# Password Entry Area (00h:122-125): 4-byte host password written
# MSB-first (big-endian) to unlock protected CDB/EEPROM access.
self.fields[consts.PASSWORD_ENTRY] = [
NumberRegField(consts.PASSWORD_ENTRY, self.getaddr(consts.PASSWORD_ENTRY_OFFSET),
format=">I", size=consts.PASSWORD_ENTRY_SIZE, ro=False),
]

# PasswordCmdResult (00h:42.3-0): result of the most recent password
# entry/change written to the Password Entry Area.
self.fields[consts.PASSWORD_CMD_RESULT] = [
NumberRegField(consts.PASSWORD_CMD_RESULT, self.getaddr(42),
*(RegBitField("Bit%d" % (bit), bit) for bit in range (0, 4))
),
]

16 changes: 14 additions & 2 deletions tests/sonic_xcvr/test_cdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ def test_enter_password_invalid(self, password, expected):
assert result == expected

def test_enter_password_valid(self):
"""Test enter_password with valid password"""
"""Test enter_password sends CDB command 0001h"""
self.handler.send_cmd = MagicMock(return_value=True)
result = self.handler.enter_password(0x00001011)
assert result is True
Expand All @@ -898,7 +898,7 @@ def test_enter_password_valid(self):
)

def test_enter_password_default(self):
"""Test enter_password with default password"""
"""Test enter_password with the default password"""
self.handler.send_cmd = MagicMock(return_value=True)
result = self.handler.enter_password()
assert result is True
Expand All @@ -907,6 +907,18 @@ def test_enter_password_default(self):
{"password": cdb_consts.CDB_DEFAULT_PASSWORD}
)

def test_enter_password_cdb_failure(self):
"""Test enter_password propagates a CDB command failure to the caller"""
self.handler.send_cmd = MagicMock(return_value=False)
assert self.handler.enter_password(0x00001011) is False

def test_enter_password_invalid_no_cmd(self):
"""Test an invalid password is rejected without sending a CDB command"""
self.handler.send_cmd = MagicMock(return_value=True)
result = self.handler.enter_password("not_an_int")
assert result is False
self.handler.send_cmd.assert_not_called()

def test_write_lpl_block(self):
"""Test write_lpl_block sends correct command"""
self.handler.send_cmd = MagicMock(return_value=True)
Expand Down
4 changes: 2 additions & 2 deletions tests/sonic_xcvr/test_cdb_fw.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def setup_method(self):
self.reader = MagicMock()
self.writer = MagicMock()
self.mem_map = MagicMock()

# Mock the parent class initialization
with patch.object(CdbFwHandler, 'initFwHandler', return_value=True):
self.handler = CdbFwHandler(self.reader, self.writer, self.mem_map)
Expand Down Expand Up @@ -441,7 +441,7 @@ def test_full_firmware_update_flow_lpl(self, mock_file):
reader = MagicMock()
writer = MagicMock()
mem_map = MagicMock()

# Mock successful initialization
with patch.object(CdbFwHandler, 'send_cmd', return_value=True):
with patch.object(CdbFwHandler, 'read_reply', return_value={
Expand Down
Loading
Loading