Skip to content

Commit 456be04

Browse files
committed
Add link event damping CLI commands (config/show/clear)
Implements CLI support for RFC 2439/7196 link event dampening: - config interface dampening enable/disable: configure AIED dampening with validation (IntRange per review feedback), monitor-only mode, and configurable flap-penalty - show interfaces dampening: display config and operational state from CONFIG_DB and STATE_DB with per-interface counters - sonic-clear interfaces dampening: reset penalty via STATE_DB flag Supersedes sonic-net#3001 with improvements: - click.IntRange validation (per @Junchao-Mellanox review) - Monitor-only mode per RFC 7196 "Calculate But Do Not Damp" - Configurable flap-penalty (vs hardcoded 1000) - Show command with operational state and counters - Clear command per RFC 2439 Section 4.8.6 Signed-off-by: DendroLabs <info@dendrolabs.com>
1 parent 9a408e6 commit 456be04

3 files changed

Lines changed: 300 additions & 0 deletions

File tree

clear/main.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from utilities_common import util_base
1212
from show.plugins.pbh import read_pbh_counters
1313
from config.plugins.pbh import serialize_pbh_counters
14+
from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector
1415
from . import plugins
1516
from . import stp
1617
# This is from the aliases example:
@@ -763,5 +764,55 @@ def asic_sdk_health_event(db, namespace):
763764
state_db.delete(state_db.STATE_DB, key);
764765

765766

767+
#
768+
# 'interfaces' group ("sonic-clear interfaces ...")
769+
#
770+
771+
@cli.group(cls=AliasedGroup)
772+
def interfaces():
773+
"""Clear interface-related state"""
774+
pass
775+
776+
777+
@interfaces.command()
778+
@click.argument('interface_name', metavar='<interface_name>', required=False)
779+
def dampening(interface_name):
780+
"""Clear link event dampening state (reset penalty to 0).
781+
782+
Without an interface name, clears dampening on all interfaces.
783+
The interface is immediately unsuppressed if currently damped.
784+
"""
785+
config_db = ConfigDBConnector()
786+
config_db.connect()
787+
port_table = config_db.get_table("PORT")
788+
789+
if interface_name:
790+
if interface_name not in port_table:
791+
click.echo("Error: Interface {} does not exist".format(interface_name))
792+
raise SystemExit(1)
793+
ports_to_clear = [interface_name]
794+
else:
795+
ports_to_clear = []
796+
for port_name, port_data in port_table.items():
797+
algo = port_data.get("link_event_damping_algorithm", "disabled")
798+
if algo != "disabled":
799+
ports_to_clear.append(port_name)
800+
801+
if not ports_to_clear:
802+
click.echo("No interfaces have dampening configured")
803+
return
804+
805+
state_db = SonicV2Connector(host="127.0.0.1")
806+
state_db.connect(state_db.STATE_DB)
807+
808+
for port_name in ports_to_clear:
809+
state_key = "CLEAR_DAMPENING|{}".format(port_name)
810+
state_db.set(state_db.STATE_DB, state_key, "clear", "true")
811+
click.echo("Cleared dampening on {}".format(port_name))
812+
813+
if not interface_name:
814+
click.echo("Cleared dampening on {} interface(s)".format(len(ports_to_clear)))
815+
816+
766817
if __name__ == '__main__':
767818
cli()

config/main.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6349,6 +6349,115 @@ def cable_length(ctx, interface_name, length):
63496349
except ValueError as e:
63506350
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
63516351

6352+
#
6353+
# 'dampening' subgroup ('config interface dampening ...')
6354+
#
6355+
6356+
@interface.group(cls=clicommon.AbbreviationGroup)
6357+
@click.pass_context
6358+
def dampening(ctx):
6359+
"""Configure link event dampening on an interface"""
6360+
pass
6361+
6362+
6363+
@dampening.command()
6364+
@click.argument('interface_name', metavar='<interface_name>', required=True)
6365+
@click.option('--half-life', type=click.IntRange(min=1, max=3600), default=5,
6366+
show_default=True,
6367+
help='Decay half-life in seconds (1-3600).')
6368+
@click.option('--reuse', type=click.IntRange(min=1, max=20000), default=1000,
6369+
show_default=True,
6370+
help='Reuse threshold (1-20000).')
6371+
@click.option('--suppress', type=click.IntRange(min=1, max=20000), default=2000,
6372+
show_default=True,
6373+
help='Suppress threshold (1-20000).')
6374+
@click.option('--max-suppress-time', type=click.IntRange(min=1, max=3600), default=20,
6375+
show_default=True,
6376+
help='Max suppress time in seconds (1-3600).')
6377+
@click.option('--flap-penalty', type=click.IntRange(min=1, max=20000), default=1000,
6378+
show_default=True,
6379+
help='Penalty per link-down event (1-20000).')
6380+
@click.option('--monitor', is_flag=True, default=False,
6381+
help='Monitor-only mode: calculate penalties and emit syslog '
6382+
'but do NOT suppress events. Use to safely tune parameters '
6383+
'in production before enabling full dampening.')
6384+
@click.pass_context
6385+
def enable(ctx, interface_name, half_life, reuse, suppress, max_suppress_time,
6386+
flap_penalty, monitor):
6387+
"""Enable link event dampening on an interface with AIED algorithm."""
6388+
config_db = ctx.obj['config_db']
6389+
6390+
if clicommon.get_interface_naming_mode() == "alias":
6391+
interface_name = interface_alias_to_name(config_db, interface_name)
6392+
if interface_name is None:
6393+
ctx.fail("'interface_name' is None!")
6394+
6395+
# Validate interface exists in PORT table
6396+
port_table = config_db.get_table("PORT")
6397+
if interface_name not in port_table:
6398+
ctx.fail("Interface {} does not exist".format(interface_name))
6399+
6400+
# Validate parameter relationships
6401+
if reuse >= suppress:
6402+
ctx.fail("Reuse threshold ({}) must be less than suppress threshold ({})".format(
6403+
reuse, suppress))
6404+
6405+
if half_life > max_suppress_time:
6406+
ctx.fail("Half-life ({}) must not exceed max-suppress-time ({})".format(
6407+
half_life, max_suppress_time))
6408+
6409+
# Calculate ceiling and warn if very high
6410+
ceiling = reuse * (2 ** (max_suppress_time / half_life))
6411+
if ceiling > 100000:
6412+
click.echo("Warning: calculated penalty ceiling is very high ({:.0f}). "
6413+
"Consider reducing max-suppress-time or increasing reuse threshold.".format(ceiling))
6414+
6415+
algorithm = "aied-monitor" if monitor else "aied"
6416+
6417+
config_db.mod_entry("PORT", interface_name, {
6418+
"link_event_damping_algorithm": algorithm,
6419+
"decay_half_life": str(half_life),
6420+
"reuse_threshold": str(reuse),
6421+
"suppress_threshold": str(suppress),
6422+
"max_suppress_time": str(max_suppress_time),
6423+
"flap_penalty": str(flap_penalty),
6424+
})
6425+
6426+
mode_str = "monitor-only" if monitor else "active"
6427+
click.echo("Link event dampening enabled on {} (algorithm={}, mode={}, half-life={}s, "
6428+
"reuse={}, suppress={}, max-suppress={}s, penalty={})".format(
6429+
interface_name, algorithm, mode_str, half_life, reuse, suppress,
6430+
max_suppress_time, flap_penalty))
6431+
6432+
6433+
@dampening.command()
6434+
@click.argument('interface_name', metavar='<interface_name>', required=True)
6435+
@click.pass_context
6436+
def disable(ctx, interface_name):
6437+
"""Disable link event dampening on an interface."""
6438+
config_db = ctx.obj['config_db']
6439+
6440+
if clicommon.get_interface_naming_mode() == "alias":
6441+
interface_name = interface_alias_to_name(config_db, interface_name)
6442+
if interface_name is None:
6443+
ctx.fail("'interface_name' is None!")
6444+
6445+
port_table = config_db.get_table("PORT")
6446+
if interface_name not in port_table:
6447+
ctx.fail("Interface {} does not exist".format(interface_name))
6448+
6449+
config_db.mod_entry("PORT", interface_name, {
6450+
"link_event_damping_algorithm": "disabled",
6451+
"decay_half_life": "0",
6452+
"reuse_threshold": "0",
6453+
"suppress_threshold": "0",
6454+
"max_suppress_time": "0",
6455+
"flap_penalty": "0",
6456+
})
6457+
6458+
click.echo("Link event dampening disabled on {}".format(interface_name))
6459+
6460+
63526461
#
63536462
# 'transceiver' subgroup ('config interface transceiver ...')
63546463
#

show/interfaces/__init__.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,146 @@ def display_phy_taps_attribute(attr_display_name, attr_json):
14461446
click.echo("")
14471447

14481448

1449+
#
1450+
# 'dampening' subcommand ("show interfaces dampening")
1451+
#
1452+
@interfaces.command()
1453+
@click.argument('interfacename', required=False)
1454+
@clicommon.pass_db
1455+
def dampening(db, interfacename):
1456+
"""Show link event dampening configuration and operational state"""
1457+
1458+
ctx = click.get_current_context()
1459+
1460+
if interfacename:
1461+
interfacename = try_convert_interfacename_from_alias(ctx, interfacename)
1462+
1463+
config_db = db.cfgdb
1464+
state_db = db.db
1465+
1466+
port_table = config_db.get_table("PORT")
1467+
1468+
if interfacename:
1469+
if interfacename not in port_table:
1470+
ctx.fail("Interface {} does not exist".format(interfacename))
1471+
ports = {interfacename: port_table[interfacename]}
1472+
else:
1473+
ports = port_table
1474+
1475+
header = [
1476+
"Interface",
1477+
"Algorithm",
1478+
"Half-Life(s)",
1479+
"Reuse",
1480+
"Suppress",
1481+
"Max-Suppress(s)",
1482+
"Penalty",
1483+
"Flap-Penalty",
1484+
"Suppressed",
1485+
"Time-Left(s)",
1486+
]
1487+
1488+
rows = []
1489+
for port_name in natsorted(ports.keys()):
1490+
port_data = ports[port_name]
1491+
algorithm = port_data.get("link_event_damping_algorithm", "disabled")
1492+
1493+
if algorithm == "disabled":
1494+
# Only show this port if specifically requested
1495+
if interfacename:
1496+
rows.append([
1497+
port_name,
1498+
"disabled",
1499+
"-", "-", "-", "-", "-", "-", "-", "-"
1500+
])
1501+
continue
1502+
1503+
is_monitor = (algorithm == "aied-monitor")
1504+
1505+
half_life = port_data.get("decay_half_life", "0")
1506+
reuse = port_data.get("reuse_threshold", "0")
1507+
suppress = port_data.get("suppress_threshold", "0")
1508+
max_suppress = port_data.get("max_suppress_time", "0")
1509+
flap_penalty = port_data.get("flap_penalty", "1000")
1510+
1511+
# Read operational state from STATE_DB
1512+
state_key = "PORT_TABLE|{}".format(port_name)
1513+
current_penalty = "N/A"
1514+
suppressed = "N/A"
1515+
time_remaining = "N/A"
1516+
1517+
if state_db:
1518+
penalty_val = state_db.get(state_db.STATE_DB, state_key,
1519+
"damping_current_penalty")
1520+
suppressed_val = state_db.get(state_db.STATE_DB, state_key,
1521+
"damping_suppressed")
1522+
time_val = state_db.get(state_db.STATE_DB, state_key,
1523+
"damping_time_remaining")
1524+
1525+
if penalty_val:
1526+
current_penalty = penalty_val
1527+
if suppressed_val:
1528+
if is_monitor and suppressed_val == "true":
1529+
suppressed = "(Mon)"
1530+
else:
1531+
suppressed = "Yes" if suppressed_val == "true" else "No"
1532+
if time_val:
1533+
time_remaining = time_val if suppressed == "Yes" else "-"
1534+
1535+
rows.append([
1536+
port_name,
1537+
algorithm,
1538+
half_life,
1539+
reuse,
1540+
suppress,
1541+
max_suppress,
1542+
current_penalty,
1543+
flap_penalty,
1544+
suppressed,
1545+
time_remaining,
1546+
])
1547+
1548+
if not rows:
1549+
if interfacename:
1550+
click.echo("Link event dampening is not configured on {}".format(
1551+
interfacename))
1552+
else:
1553+
click.echo("Link event dampening is not configured on any interface")
1554+
return
1555+
1556+
click.echo(tabulate(rows, header, tablefmt="simple"))
1557+
click.echo("")
1558+
1559+
# Show counters if a specific interface is queried
1560+
if interfacename and state_db:
1561+
_show_dampening_counters(state_db, interfacename)
1562+
1563+
1564+
def _show_dampening_counters(state_db, interface_name):
1565+
"""Display per-interface dampening event counters."""
1566+
state_key = "PORT_TABLE|{}".format(interface_name)
1567+
1568+
counter_fields = [
1569+
("damping_pre_transitions", "Pre-damping transitions (total)"),
1570+
("damping_post_transitions", "Post-damping transitions (total)"),
1571+
("damping_pre_up_transitions", "Pre-damping UP transitions"),
1572+
("damping_pre_down_transitions", "Pre-damping DOWN transitions"),
1573+
("damping_post_up_transitions", "Post-damping UP transitions"),
1574+
("damping_post_down_transitions", "Post-damping DOWN transitions"),
1575+
]
1576+
1577+
counter_rows = []
1578+
for field, description in counter_fields:
1579+
value = state_db.get(state_db.STATE_DB, state_key, field)
1580+
if value:
1581+
counter_rows.append([description, value])
1582+
1583+
if counter_rows:
1584+
click.echo("Dampening Counters for {}:".format(interface_name))
1585+
click.echo(tabulate(counter_rows, ["Counter", "Value"], tablefmt="simple"))
1586+
click.echo("")
1587+
1588+
14491589
@interfaces.command('phy-serdes')
14501590
@click.argument('interfacename', required=True)
14511591
@multi_asic_util.multi_asic_click_options

0 commit comments

Comments
 (0)