Skip to content

Add transceiver pm CLI for advanced window-based performance monitoring #2927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
36 changes: 20 additions & 16 deletions doc/Command-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1100,26 +1100,30 @@ This command displays information for all the interfaces for the transceiver req

- Example (Display performance monitoring info of SFP transceiver connected to Ethernet100):
```
admin@sonic:~$ show interfaces transceiver pm Ethernet100
admin@sonic:~$ show interfaces transceiver pm current 60sec Ethernet100
Wed Jul 31 17:13:51 UTC 2023
PM window: 60sec
Ethernet100:
PM window start time: Mon Jul 31 17:12:49 UTC 2023
PM window end time: Mon Jul 31 17:13:50 UTC 2023
Parameter Unit Min Avg Max Threshold Threshold Threshold Threshold Threshold Threshold
High High Crossing Low Low Crossing
Alarm Warning Alert-High Alarm Warning Alert-Low
High High Crossing Low Low Crossing
Alarm Warning Alert-High Alarm Warning Alert-Low
--------------- ------ -------- -------- -------- ----------- ----------- ------------ ----------- ----------- -----------
Tx Power dBm -8.22 -8.23 -8.24 -5.0 -6.0 False -16.99 -16.003 False
Rx Total Power dBm -10.61 -10.62 -10.62 2.0 0.0 False -21.0 -18.0 False
Rx Signal Power dBm -40.0 0.0 40.0 13.0 10.0 True -18.0 -15.0 True
CD-short link ps/nm 0.0 0.0 0.0 1000.0 500.0 False -1000.0 -500.0 False
PDL dB 0.5 0.6 0.6 4.0 4.0 False 0.0 0.0 False
OSNR dB 36.5 36.5 36.5 99.0 99.0 False 0.0 0.0 False
eSNR dB 30.5 30.5 30.5 99.0 99.0 False 0.0 0.0 False
CFO MHz 54.0 70.0 121.0 3800.0 3800.0 False -3800.0 -3800.0 False
DGD ps 5.37 5.56 5.81 7.0 7.0 False 0.0 0.0 False
SOPMD ps^2 0.0 0.0 0.0 655.35 655.35 False 0.0 0.0 False
SOP ROC krad/s 1.0 1.0 2.0 N/A N/A N/A N/A N/A N/A
Pre-FEC BER N/A 4.58E-04 4.66E-04 5.76E-04 1.25E-02 1.10E-02 0.0 0.0 0.0 0.0
Tx Power dBm -8.19 -8.17 -8.15 -5.0 -6.0 False -16.99 -16.003 False
Rx Total Power dBm -8.61 -8.56 -8.51 2.0 0.0 False -21.0 -18.0 False
Rx Signal Power dBm -9.12 -9.09 -9.07 13.0 10.0 False -18.0 -15.0 False
CD-short link ps/nm -2.0 -1.0 0.0 1000.0 500.0 False -1000.0 -500.0 False
PDL dB 0.3 0.3 0.4 4.0 4.0 False 0.0 0.0 False
OSNR dB 36.4 36.4 36.4 99.0 99.0 False 0.0 0.0 False
eSNR dB 17.9 18.0 18.2 99.0 99.0 False 0.0 0.0 False
CFO MHz -776.0 -659.0 -546.0 3800.0 3800.0 False -3800.0 -3800.0 False
DGD ps 2.0 2.0 2.0 7.0 7.0 False 0.0 0.0 False
SOPMD ps^2 26.0 34.0 44.0 655.35 655.35 False 0.0 0.0 False
SOP ROC krad/s 0.0 0.0 0.0 N/A N/A N/A N/A N/A N/A
Pre-FEC BER N/A 5.37E-04 5.55E-04 5.80E-04 1.25E-02 1.10E-02 False 0.0 0.0 False
Post-FEC BER N/A 0.0 0.0 0.0 1000.0 1.0 False 0.0 0.0 False
EVM % 100.0 100.0 100.0 N/A N/A N/A N/A N/A N/A
EVM % 0.0 0.0 0.0 N/A N/A N/A N/A N/A N/A
```

- Example (Display status info of SFP transceiver connected to Ethernet100):
Expand Down
111 changes: 93 additions & 18 deletions scripts/sfpshow
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import ast
import os
import re
import sys
import time
from typing import Dict
from datetime import datetime

import click
from natsort import natsorted
Expand Down Expand Up @@ -262,7 +264,12 @@ def display_invalid_intf_status(intf_name):
click.echo(output)

class SFPShow(object):
def __init__(self, intf_name, namespace_option, dump_dom=False):
PM_WINDOW_NUM_IN_DB = {'60sec': 15, '15min': 12, '24hrs': 2}
PM_WINDOW_START_INDEX_IN_DB = {'60sec': 1, '15min': 16, '24hrs': 28}
PM_TABLE_NAME_IN_DB = 'TRANSCEIVER_PM_WINDOW_STATS'

def __init__(self, intf_name, namespace_option, dump_dom=False,
pm_window_size=None, pm_window_index=None):
super(SFPShow, self).__init__()
self.db = None
self.intf_name = intf_name
Expand All @@ -272,6 +279,8 @@ class SFPShow(object):
self.intf_pm: Dict[str, str] = {}
self.intf_status: Dict[str, str] = {}
self.multi_asic = multi_asic_util.MultiAsic(namespace_option=namespace_option)
self.pm_window_size = pm_window_size
self.pm_window_index = pm_window_index

# Convert dict values to cli output string
def format_dict_value_to_string(self, sorted_key_table,
Expand Down Expand Up @@ -490,16 +499,56 @@ class SFPShow(object):
def beautify_pm_field(self, prefix, field):
if field is None:
return 'N/A'
elif prefix in {'prefec_ber'}:
elif prefix in {'prefec_ber'} and isinstance(field, float):
return "{:.2E}".format(field) if field != 0 else '0.0'
else:
return str(field)

def convert_interface_sfp_pm_to_cli_output_string(self, state_db, interface_name):
sfp_pm_dict = state_db.get_all(
self.db.STATE_DB, 'TRANSCEIVER_PM|{}'.format(interface_name))
sfp_threshold_dict = state_db.get_all(
state_db.STATE_DB, 'TRANSCEIVER_DOM_THRESHOLD|{}'.format(interface_name))
def beautify_pm_timestamp(self, secs_since_epoch):
if secs_since_epoch is None:
return 'N/A'
dt_object = datetime.utcfromtimestamp(secs_since_epoch)
formatted_time = dt_object.strftime("%a %b %d %H:%M:%S UTC %Y")
return formatted_time

def get_pm_dict_for_specified_window(self, db, interface_name,
window_size, window_index):
"""Get PM dict for specified window size and index"""
pm_windows_dict = db.get_all(
db.STATE_DB, '{}|{}'.format(self.PM_TABLE_NAME_IN_DB, interface_name))
if not pm_windows_dict:
return None
window_num = self.PM_WINDOW_NUM_IN_DB[window_size]
start_win_idx_in_db = self.PM_WINDOW_START_INDEX_IN_DB[window_size]
end_win_idx_in_db = start_win_idx_in_db + window_num - 1
pm_windows_for_win_size = []
cur_win_idx = -1
for i in range(start_win_idx_in_db, end_win_idx_in_db + 1):
win_name = 'window' + str(i)
if (win_name not in pm_windows_dict or
pm_windows_dict[win_name] == 'N/A'):
pm_dict = None
else:
# Convert string to dict
pm_dict = ast.literal_eval(pm_windows_dict[win_name])
pm_windows_for_win_size.append(pm_dict)
if (pm_dict is not None and
'pm_win_current' in pm_dict and
pm_dict['pm_win_current'] == 'true'):
cur_win_idx = i
if cur_win_idx == -1:
return None
return pm_windows_for_win_size[(cur_win_idx -
start_win_idx_in_db -
window_index +
window_num) % window_num]

def convert_interface_sfp_pm_to_cli_output_string(self, db, interface_name,
window_size, window_index):
sfp_pm_dict = self.get_pm_dict_for_specified_window(
db, interface_name, window_size, window_index)
sfp_threshold_dict = db.get_all(
db.STATE_DB, 'TRANSCEIVER_DOM_THRESHOLD|{}'.format(interface_name))
table = []
indent_num = 4
indent = ' ' * indent_num
Expand All @@ -517,8 +566,10 @@ class SFPShow(object):
for suffix in ZR_PM_THRESHOLD_KEY_SUFFIXS:
key = self.convert_pm_prefix_to_threshold_prefix(
prefix) + suffix
thresholds.append(
float(sfp_threshold_dict[key]) if key in sfp_threshold_dict else None)
threshold = None
if key in sfp_threshold_dict and sfp_threshold_dict[key] != 'N/A':
threshold = float(sfp_threshold_dict[key])
thresholds.append(threshold)

tca_high, tca_low = None, None
if values[2] is not None and thresholds[0] is not None:
Expand All @@ -534,7 +585,13 @@ class SFPShow(object):

output += tabulate(table,
ZR_PM_HEADER, disable_numparse=True).replace('\n', '\n' + indent)
output += '\n'
start_timestamp = self.beautify_pm_timestamp(
float(sfp_pm_dict.get('pm_win_start_time', None)))
end_timestamp = self.beautify_pm_timestamp(
float(sfp_pm_dict.get('pm_win_end_time', None)))
start_timestamp_output = '\n{}PM window start time: {}'.format(indent, start_timestamp)
end_timestamp_output = '\n{}PM window end time: {}'.format(indent, end_timestamp)
output = start_timestamp_output + end_timestamp_output + output + '\n'
else:
output = ZR_PM_NOT_APPLICABLE_STR + '\n'
return output
Expand Down Expand Up @@ -582,14 +639,14 @@ class SFPShow(object):
def get_pm(self):
if self.intf_name is not None:
self.intf_pm[self.intf_name] = self.convert_interface_sfp_pm_to_cli_output_string(
self.db, self.intf_name)
self.db, self.intf_name, self.pm_window_size, self.pm_window_index)
else:
port_table_keys = self.db.keys(self.db.APPL_DB, "PORT_TABLE:*")
for i in port_table_keys:
interface = re.split(':', i, maxsplit=1)[-1].strip()
if interface and interface.startswith(front_panel_prefix()) and not interface.startswith((backplane_prefix(), inband_prefix(), recirc_prefix())):
self.intf_pm[interface] = self.convert_interface_sfp_pm_to_cli_output_string(
self.db, interface)
self.db, interface, self.pm_window_size, self.pm_window_index)

@multi_asic_util.run_on_multi_asic
def get_status(self):
Expand All @@ -613,8 +670,10 @@ class SFPShow(object):
click.echo(tabulate(sorted_port_table, header))

def display_pm(self):
click.echo(
"\n".join([f"{k}: {v}" for k, v in natsorted(self.intf_pm.items())]))
output = self.beautify_pm_timestamp(time.time()) + '\n'
output += 'PM window: {}\n'.format(self.pm_window_size)
output += "\n".join([f"{k}: {v}" for k, v in natsorted(self.intf_pm.items())])
click.echo(output)

def display_status(self):
click.echo(
Expand Down Expand Up @@ -684,18 +743,34 @@ def presence(port, namespace):
# 'pm' subcommand


@cli.command()
@click.option('-p', '--port', metavar='<port_name>', help="Display SFP PM for port <port_name> only")
@cli.command(help="Display performance monitoring information")
@click.option('-s', '--window_size',
type=click.Choice(['60sec', '15min', '24hrs']),
required=True,
help="Display data for the specified window size")
@click.option('-i', '--window_index',
type=int,
required=True,
help="Display data for the specified window index")
@click.option('-p', '--port', metavar='<port_name>', help="Display data for port <port_name> only")
@click.option('-n', '--namespace', default=None, help="Display interfaces for specific namespace")
def pm(port, namespace):
def pm(window_size, window_index, port, namespace):
# Validate window_index based on window_size
if window_size == '60sec' and not 0 <= window_index <= 14:
raise click.BadArgumentUsage("For 60sec window size, window_index should be between 0 and 14.")
elif window_size == '15min' and not 0 <= window_index <= 11:
raise click.BadArgumentUsage("For 15min window size, window_index should be between 0 and 11.")
elif window_size == '24hrs' and not 0 <= window_index <= 1:
raise click.BadArgumentUsage("For 24hrs window size, window_index should be between 0 and 1.")

if port and multi_asic.is_multi_asic() and namespace is None:
try:
namespace = multi_asic.get_namespace_for_port(port)
except Exception:
display_invalid_intf_pm(port)
sys.exit(1)

sfp = SFPShow(port, namespace)
sfp = SFPShow(port, namespace, False, window_size, window_index)
sfp.get_pm()
sfp.display_pm()

Expand Down
65 changes: 57 additions & 8 deletions show/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,16 +446,25 @@ def eeprom(interfacename, dump_dom, namespace, verbose):

clicommon.run_command(cmd, display_cmd=verbose)

@transceiver.command()
@click.argument('interfacename', required=False)
@click.option('--namespace', '-n', 'namespace', default=None, show_default=True,
type=click.Choice(multi_asic_util.multi_asic_ns_choices()), help='Namespace name or all')
@click.option('--verbose', is_flag=True, help="Enable verbose output")
def pm(interfacename, namespace, verbose):
@transceiver.group(help="Display performance monitoring information")
def pm():
"""Show interface transceiver performance monitoring information"""
pass

ctx = click.get_current_context()

def cli_pm_helper(window_size, window_index, interfacename, namespace, verbose, ctx):
"""
Helper function for pm subcommand

Parameters:
interfacename (str): Name of the port to retrieve PM information for.
namespace (str): Name of the namespace in which the port resides.
window_size (str): Size of the window to consider for PM information.
window_index (int): Index of the window to consider for PM information.
Starts from 0, which typically denotes the most
recent window.
verbose (bool): Whether to display verbose output.
ctx (click.Context): Click context object.
"""
cmd = ['sfpshow', 'pm']

if interfacename is not None:
Expand All @@ -467,8 +476,48 @@ def pm(interfacename, namespace, verbose):
if namespace is not None:
cmd += ['-n', str(namespace)]

cmd += ['-s', window_size, '-i', str(window_index)]
clicommon.run_command(cmd, display_cmd=verbose)

@pm.command(help="Display current PM data for the specified window size")
@click.argument('window_size', type=click.Choice(['60sec', '15min', '24hrs']))
@click.argument('interfacename', required=False)
@click.option('--namespace', '-n', 'namespace', default=None, show_default=True,
type=click.Choice(multi_asic_util.multi_asic_ns_choices()),
help='Namespace name or all')
@click.option('--verbose', is_flag=True, help="Enable verbose output")
def current(window_size, interfacename, namespace, verbose):
"""Show interface transceiver pm current subcommand"""
ctx = click.get_current_context()
cli_pm_helper(window_size, 0, interfacename, namespace, verbose, ctx)

@pm.group(help="Display history PM data for the specified window size")
@click.argument('window_size', type=click.Choice(['60sec', '15min', '24hrs']))
def history(window_size):
"""Show interface transceiver pm history subcommand <window_size>"""
ctx = click.get_current_context()
ctx.obj = {'window_size': window_size}

@history.command(help="Display history PM data for the specified window index, starting from 1")
@click.argument('window_index', type=int)
@click.argument('interfacename', required=False)
@click.option('--namespace', '-n', 'namespace', default=None, show_default=True,
type=click.Choice(multi_asic_util.multi_asic_ns_choices()),
help='Namespace name or all')
@click.option('--verbose', is_flag=True, help="Enable verbose output")
def window(window_index, interfacename, namespace, verbose):
"""Show interface transceiver pm history <window_size> window <window_indx> subcommand"""
ctx = click.get_current_context()
window_size = ctx.obj['window_size']
if window_size == '60sec' and not 1 <= window_index <= 14:
raise click.BadArgumentUsage("For 60sec window size, window_index should be between 1 and 14.")
elif window_size == '15min' and not 1 <= window_index <= 11:
raise click.BadArgumentUsage("For 15min window size, window_index should be between 1 and 11.")
elif window_size == '24hrs' and window_index != 1:
raise click.BadArgumentUsage("For 24hrs window size, window_index should be 1.")
cli_pm_helper(window_size, window_index, interfacename, namespace, verbose, ctx)


@transceiver.command('status') # 'status' is the actual sub-command name under 'transceiver' command
@click.argument('interfacename', required=False)
@click.option('--namespace', '-n', 'namespace', default=None, show_default=True,
Expand Down
Loading