Skip to content

Commit 38f8c06

Browse files
authored
[sfputil] Expose error status fetched from STATE_DB or platform API to CLI (#1658)
Expose error status fetched from STATE_DB or platform API to CLI. The command is - `sfputil show error-status [-hw|--fetch-from-hardware] [<interface_name>]` - and `show interfaces transceiver error-status [-hw|--fetch-from-hardware] [<interface_name>]` The error status will be fetched from - `STATE_DB` by default - hardware via platform API if the parameter `--fetch-from-hardware` is provided. In this case, the CLI will call platform API in the pmon docker and format the output. Signed-off-by: Stephen Sun <stephens@nvidia.com>
1 parent c5d00ae commit 38f8c06

File tree

5 files changed

+203
-1
lines changed

5 files changed

+203
-1
lines changed

doc/Command-Reference.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,7 @@ This command displays information for all the interfaces for the transceiver req
883883

884884
- Usage:
885885
```
886-
show interfaces transceiver (eeprom [-d|--dom] | lpmode | presence) [<interface_name>]
886+
show interfaces transceiver (eeprom [-d|--dom] | lpmode | presence | error-status [-hw|--fetch-from-hardware]) [<interface_name>]
887887
```
888888

889889
- Example (Decode and display information stored on the EEPROM of SFP transceiver connected to Ethernet0):
@@ -937,6 +937,15 @@ This command displays information for all the interfaces for the transceiver req
937937
----------- ----------
938938
Ethernet100 Present
939939
```
940+
941+
- Example (Display error status of SFP transceiver connected to Ethernet100):
942+
```
943+
admin@sonic:~$ show interfaces transceiver error-status Ethernet100
944+
Port Error Status
945+
----------- --------------
946+
Ethernet100 OK
947+
```
948+
940949
Go Back To [Beginning of the document](#) or [Beginning of this section](#basic-show-commands)
941950

942951
## AAA & TACACS+

sfputil/main.py

+134
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77

88
import os
99
import sys
10+
import natsort
11+
import ast
1012

13+
import subprocess
1114
import click
1215
import sonic_platform
1316
import sonic_platform_base.sonic_sfp.sfputilhelper
17+
from swsscommon.swsscommon import SonicV2Connector
1418
from natsort import natsorted
1519
from sonic_py_common import device_info, logger, multi_asic
1620
from tabulate import tabulate
@@ -615,6 +619,136 @@ def presence(port):
615619
click.echo(tabulate(output_table, table_header, tablefmt="simple"))
616620

617621

622+
# 'error-status' subcommand
623+
def fetch_error_status_from_platform_api(port):
624+
"""Fetch the error status from platform API and return the output as a string
625+
Args:
626+
port: the port whose error status will be fetched.
627+
None represents for all ports.
628+
Returns:
629+
A string consisting of the error status of each port.
630+
"""
631+
if port is None:
632+
logical_port_list = natsort.natsorted(platform_sfputil.logical)
633+
# Create a list containing the logical port names of all ports we're interested in
634+
generate_sfp_list_code = \
635+
"sfp_list = chassis.get_all_sfps()\n"
636+
else:
637+
physical_port_list = logical_port_name_to_physical_port_list(port)
638+
logical_port_list = [port]
639+
# Create a list containing the logical port names of all ports we're interested in
640+
generate_sfp_list_code = \
641+
"sfp_list = [chassis.get_sfp(x) for x in {}]\n".format(physical_port_list)
642+
643+
# Code to initialize chassis object
644+
init_chassis_code = \
645+
"import sonic_platform.platform\n" \
646+
"platform = sonic_platform.platform.Platform()\n" \
647+
"chassis = platform.get_chassis()\n"
648+
649+
# Code to fetch the error status
650+
get_error_status_code = \
651+
"try:\n"\
652+
" errors=['{}:{}'.format(sfp.index, sfp.get_error_description()) for sfp in sfp_list]\n" \
653+
"except NotImplementedError as e:\n"\
654+
" errors=['{}:{}'.format(sfp.index, 'OK (Not implemented)') for sfp in sfp_list]\n" \
655+
"print(errors)\n"
656+
657+
get_error_status_command = "docker exec pmon python3 -c \"{}{}{}\"".format(
658+
init_chassis_code, generate_sfp_list_code, get_error_status_code)
659+
# Fetch error status from pmon docker
660+
try:
661+
output = subprocess.check_output(get_error_status_command, shell=True, universal_newlines=True)
662+
except subprocess.CalledProcessError as e:
663+
click.Abort("Error! Unable to fetch error status for SPF modules. Error code = {}, error messages: {}".format(e.returncode, e.output))
664+
return None
665+
666+
output_list = output.split('\n')
667+
for output_str in output_list:
668+
# The output of all SFP error status are a list consisting of element with convention of '<sfp no>:<error status>'
669+
# Besides, there can be some logs captured during the platform API executing
670+
# So, first of all, we need to skip all the logs until find the output list of SFP error status
671+
if output_str[0] == '[' and output_str[-1] == ']':
672+
output_list = ast.literal_eval(output_str)
673+
break
674+
675+
output_dict = {}
676+
for output in output_list:
677+
sfp_index, error_status = output.split(':')
678+
output_dict[int(sfp_index)] = error_status
679+
680+
output = []
681+
for logical_port_name in logical_port_list:
682+
physical_port_list = logical_port_name_to_physical_port_list(logical_port_name)
683+
port_name = get_physical_port_name(logical_port_name, 1, False)
684+
685+
output.append([port_name, output_dict.get(physical_port_list[0])])
686+
687+
return output
688+
689+
def fetch_error_status_from_state_db(port, state_db):
690+
"""Fetch the error status from STATE_DB and return them in a list.
691+
Args:
692+
port: the port whose error status will be fetched.
693+
None represents for all ports.
694+
Returns:
695+
A list consisting of tuples (port, description) and sorted by port.
696+
"""
697+
status = {}
698+
if port:
699+
status[port] = state_db.get_all(state_db.STATE_DB, 'TRANSCEIVER_STATUS|{}'.format(port))
700+
else:
701+
ports = state_db.keys(state_db.STATE_DB, 'TRANSCEIVER_STATUS|*')
702+
for key in ports:
703+
status[key.split('|')[1]] = state_db.get_all(state_db.STATE_DB, key)
704+
705+
sorted_ports = natsort.natsorted(status)
706+
output = []
707+
for port in sorted_ports:
708+
statestring = status[port].get('status')
709+
description = status[port].get('error')
710+
if statestring == '1':
711+
description = 'OK'
712+
elif statestring == '0':
713+
description = 'Unplugged'
714+
elif description == 'N/A':
715+
log.log_error("Inconsistent state found for port {}: state is {} but error description is N/A".format(port, statestring))
716+
description = 'Unknown state: {}'.format(statestring)
717+
718+
output.append([port, description])
719+
720+
return output
721+
722+
@show.command()
723+
@click.option('-p', '--port', metavar='<port_name>', help="Display SFP error status for port <port_name> only")
724+
@click.option('-hw', '--fetch-from-hardware', 'fetch_from_hardware', is_flag=True, default=False, help="Fetch the error status from hardware directly")
725+
def error_status(port, fetch_from_hardware):
726+
"""Display error status of SFP transceiver(s)"""
727+
output_table = []
728+
table_header = ["Port", "Error Status"]
729+
730+
# Create a list containing the logical port names of all ports we're interested in
731+
if port and platform_sfputil.is_logical_port(port) == 0:
732+
click.echo("Error: invalid port '{}'\n".format(port))
733+
click.echo("Valid values for port: {}\n".format(str(platform_sfputil.logical)))
734+
sys.exit(ERROR_INVALID_PORT)
735+
736+
if fetch_from_hardware:
737+
output_table = fetch_error_status_from_platform_api(port)
738+
else:
739+
# Connect to STATE_DB
740+
state_db = SonicV2Connector(host='127.0.0.1')
741+
if state_db is not None:
742+
state_db.connect(state_db.STATE_DB)
743+
else:
744+
click.echo("Failed to connect to STATE_DB")
745+
return
746+
747+
output_table = fetch_error_status_from_state_db(port, state_db)
748+
749+
click.echo(tabulate(output_table, table_header, tablefmt='simple'))
750+
751+
618752
# 'lpmode' subcommand
619753
@show.command()
620754
@click.option('-p', '--port', metavar='<port_name>', help="Display SFP low-power mode status for port <port_name> only")

show/interfaces/__init__.py

+27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import os
33

4+
import subprocess
45
import click
56
import utilities_common.cli as clicommon
67
import utilities_common.multi_asic as multi_asic_util
@@ -10,6 +11,7 @@
1011
from sonic_py_common import device_info
1112
from swsscommon.swsscommon import ConfigDBConnector
1213
from portconfig import get_child_ports
14+
import sonic_platform_base.sonic_sfp.sfputilhelper
1315

1416
from . import portchannel
1517
from collections import OrderedDict
@@ -396,6 +398,31 @@ def presence(db, interfacename, namespace, verbose):
396398
clicommon.run_command(cmd, display_cmd=verbose)
397399

398400

401+
@transceiver.command()
402+
@click.argument('interfacename', required=False)
403+
@click.option('--fetch-from-hardware', '-hw', 'fetch_from_hardware', is_flag=True, default=False)
404+
@click.option('--namespace', '-n', 'namespace', default=None, show_default=True,
405+
type=click.Choice(multi_asic_util.multi_asic_ns_choices()), help='Namespace name or all')
406+
@click.option('--verbose', is_flag=True, help="Enable verbose output")
407+
@clicommon.pass_db
408+
def error_status(db, interfacename, fetch_from_hardware, namespace, verbose):
409+
""" Show transceiver error-status """
410+
411+
ctx = click.get_current_context()
412+
413+
cmd = "sudo sfputil show error-status"
414+
415+
if interfacename is not None:
416+
interfacename = try_convert_interfacename_from_alias(ctx, interfacename)
417+
418+
cmd += " -p {}".format(interfacename)
419+
420+
if fetch_from_hardware:
421+
cmd += " -hw"
422+
423+
clicommon.run_command(cmd, display_cmd=verbose)
424+
425+
399426
#
400427
# counters group ("show interfaces counters ...")
401428
#

tests/mock_tables/state_db.json

+16
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,22 @@
133133
"txpowerlowalarm": "-10.5012",
134134
"txpowerlowwarning": "-7.5007"
135135
},
136+
"TRANSCEIVER_STATUS|Ethernet0": {
137+
"status": "67",
138+
"error": "Blocking Error|High temperature"
139+
},
140+
"TRANSCEIVER_STATUS|Ethernet4": {
141+
"status": "1",
142+
"error": "N/A"
143+
},
144+
"TRANSCEIVER_STATUS|Ethernet8": {
145+
"status": "0",
146+
"error": "N/A"
147+
},
148+
"TRANSCEIVER_STATUS|Ethernet12": {
149+
"status": "255",
150+
"error": "N/A"
151+
},
136152
"CHASSIS_INFO|chassis 1": {
137153
"psu_num": "2"
138154
},

tests/sfputil_test.py

+16
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import os
33
from unittest import mock
44

5+
from .mock_tables import dbconnector
6+
57
import pytest
68
from click.testing import CliRunner
9+
from utilities_common.db import Db
710

811
test_path = os.path.dirname(os.path.abspath(__file__))
912
modules_path = os.path.dirname(test_path)
@@ -173,3 +176,16 @@ def test_version(self):
173176
runner = CliRunner()
174177
result = runner.invoke(sfputil.cli.commands['version'], [])
175178
assert result.output.rstrip() == 'sfputil version {}'.format(sfputil.VERSION)
179+
180+
def test_error_status_from_db(self):
181+
db = Db()
182+
expected_output = [['Ethernet0', 'Blocking Error|High temperature'],
183+
['Ethernet4', 'OK'],
184+
['Ethernet8', 'Unplugged'],
185+
['Ethernet12', 'Unknown state: 255']]
186+
output = sfputil.fetch_error_status_from_state_db(None, db.db)
187+
assert output == expected_output
188+
189+
expected_output_ethernet0 = expected_output[:1]
190+
output = sfputil.fetch_error_status_from_state_db('Ethernet0', db.db)
191+
assert output == expected_output_ethernet0

0 commit comments

Comments
 (0)