Skip to content

Commit

Permalink
Add trap flow counter support (sonic-net#1868)
Browse files Browse the repository at this point in the history
Add flowcnt commands
* counterpoll flowcnt-trap enable/disable
* counterpoll flowcnt-trap interval
* show flowcnt-trap stats
  • Loading branch information
Junchao-Mellanox authored Nov 25, 2021
1 parent ef82f00 commit c05845d
Show file tree
Hide file tree
Showing 14 changed files with 641 additions and 4 deletions.
8 changes: 8 additions & 0 deletions clear/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,14 @@ def statistics(db):
def remap_keys(dict):
return [{'key': k, 'value': v} for k, v in dict.items()]

# ("sonic-clear flowcnt-trap")
@cli.command()
def flowcnt_trap():
""" Clear trap flow counters """
command = "flow_counters_stat -c -t trap"
run_command(command)


# Load plugins and register them
helper = util_base.UtilHelper()
helper.load_and_register_plugins(plugins, cli)
Expand Down
5 changes: 4 additions & 1 deletion config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5948,7 +5948,7 @@ def rate():

@rate.command()
@click.argument('interval', metavar='<interval>', type=click.IntRange(min=1, max=1000), required=True)
@click.argument('rates_type', type=click.Choice(['all', 'port', 'rif']), default='all')
@click.argument('rates_type', type=click.Choice(['all', 'port', 'rif', 'flowcnt-trap']), default='all')
def smoothing_interval(interval, rates_type):
"""Set rates smoothing interval """
counters_db = swsssdk.SonicV2Connector()
Expand All @@ -5962,6 +5962,9 @@ def smoothing_interval(interval, rates_type):
if rates_type in ['rif', 'all']:
counters_db.set('COUNTERS_DB', 'RATES:RIF', 'RIF_SMOOTH_INTERVAL', interval)
counters_db.set('COUNTERS_DB', 'RATES:RIF', 'RIF_ALPHA', alpha)
if rates_type in ['flowcnt-trap', 'all']:
counters_db.set('COUNTERS_DB', 'RATES:TRAP', 'TRAP_SMOOTH_INTERVAL', interval)
counters_db.set('COUNTERS_DB', 'RATES:TRAP', 'TRAP_ALPHA', alpha)


# Load plugins and register them
Expand Down
40 changes: 38 additions & 2 deletions counterpoll/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ def disable():
# Port counter commands
@cli.group()
def port():
""" Queue counter commands """
""" Port counter commands """

@port.command()
@click.argument('poll_interval', type=click.IntRange(100, 30000))
def interval(poll_interval):
""" Set queue counter query interval """
""" Set port counter query interval """
configdb = ConfigDBConnector()
configdb.connect()
port_info = {}
Expand Down Expand Up @@ -314,6 +314,39 @@ def disable():
tunnel_info['FLEX_COUNTER_STATUS'] = DISABLE
configdb.mod_entry("FLEX_COUNTER_TABLE", "TUNNEL", tunnel_info)

# Trap flow counter commands
@cli.group()
@click.pass_context
def flowcnt_trap(ctx):
""" Trap flow counter commands """
ctx.obj = ConfigDBConnector()
ctx.obj.connect()

@flowcnt_trap.command()
@click.argument('poll_interval', type=click.IntRange(1000, 30000))
@click.pass_context
def interval(ctx, poll_interval):
""" Set trap flow counter query interval """
fc_info = {}
fc_info['POLL_INTERVAL'] = poll_interval
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_TRAP", fc_info)

@flowcnt_trap.command()
@click.pass_context
def enable(ctx):
""" Enable trap flow counter query """
fc_info = {}
fc_info['FLEX_COUNTER_STATUS'] = 'enable'
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_TRAP", fc_info)

@flowcnt_trap.command()
@click.pass_context
def disable(ctx):
""" Disable trap flow counter query """
fc_info = {}
fc_info['FLEX_COUNTER_STATUS'] = 'disable'
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_TRAP", fc_info)

@cli.command()
def show():
""" Show the counter configuration """
Expand All @@ -329,6 +362,7 @@ def show():
buffer_pool_wm_info = configdb.get_entry('FLEX_COUNTER_TABLE', BUFFER_POOL_WATERMARK)
acl_info = configdb.get_entry('FLEX_COUNTER_TABLE', ACL)
tunnel_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'TUNNEL')
trap_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'FLOW_CNT_TRAP')

header = ("Type", "Interval (in ms)", "Status")
data = []
Expand All @@ -352,6 +386,8 @@ def show():
data.append([ACL, pg_drop_info.get("POLL_INTERVAL", DEFLT_10_SEC), acl_info.get("FLEX_COUNTER_STATUS", DISABLE)])
if tunnel_info:
data.append(["TUNNEL_STAT", rif_info.get("POLL_INTERVAL", DEFLT_10_SEC), rif_info.get("FLEX_COUNTER_STATUS", DISABLE)])
if trap_info:
data.append(["FLOW_CNT_TRAP_STAT", trap_info.get("POLL_INTERVAL", DEFLT_10_SEC), trap_info.get("FLEX_COUNTER_STATUS", DISABLE)])

click.echo(tabulate(data, headers=header, tablefmt="simple", missingval=""))

Expand Down
283 changes: 283 additions & 0 deletions scripts/flow_counters_stat
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
#!/usr/bin/env python3

import argparse
import os
import _pickle as pickle
import sys

from natsort import natsorted
from tabulate import tabulate

# mock the redis for unit test purposes #
try:
if os.environ["UTILITIES_UNIT_TESTING"] == "2":
modules_path = os.path.join(os.path.dirname(__file__), "..")
tests_path = os.path.join(modules_path, "tests")
sys.path.insert(0, modules_path)
sys.path.insert(0, tests_path)
import mock_tables.dbconnector
if os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] == "multi_asic":
import mock_tables.mock_multi_asic
mock_tables.dbconnector.load_namespace_config()

except KeyError:
pass

import utilities_common.multi_asic as multi_asic_util
from utilities_common.netstat import format_number_with_comma, table_as_json, ns_diff, format_prate

# Flow counter meta data, new type of flow counters can extend this dictinary to reuse existing logic
flow_counter_meta = {
'trap': {
'headers': ['Trap Name', 'Packets', 'Bytes', 'PPS'],
'name_map': 'COUNTERS_TRAP_NAME_MAP',
}
}
flow_counters_fields = ['SAI_COUNTER_STAT_PACKETS', 'SAI_COUNTER_STAT_BYTES']

# Only do diff for 'Packets' and 'Bytes'
diff_column_positions = set([0, 1])

FLOW_COUNTER_TABLE_PREFIX = "COUNTERS:"
RATES_TABLE_PREFIX = 'RATES:'
PPS_FIELD = 'RX_PPS'
STATUS_NA = 'N/A'


class FlowCounterStats(object):
def __init__(self, args):
self.db = None
self.multi_asic = multi_asic_util.MultiAsic(namespace_option=args.namespace)
self.args = args
meta_data = flow_counter_meta[args.type]
self.name_map = meta_data['name_map']
self.headers = meta_data['headers']
self.data_file = os.path.join('/tmp/{}-stats-{}'.format(args.type, os.getuid()))
if self.args.delete and os.path.exists(self.data_file):
os.remove(self.data_file)
self.data = {}

def show(self):
"""Show flow counter statistic
"""
self._collect_and_diff()
headers, table = self._prepare_show_data()
self._print_data(headers, table)

def _collect_and_diff(self):
"""Collect statistic from db and diff from old data if any
"""
self._collect()
old_data = self._load()
need_update_cache = self._diff(old_data, self.data)
if need_update_cache:
self._save(old_data)

def _adjust_headers(self, headers):
"""Adjust table headers based on platforms
Args:
headers (list): Original headers
Returns:
headers (list): Headers with 'ASIC ID' column if it is a multi ASIC platform
"""
return ['ASIC ID'] + headers if self.multi_asic.is_multi_asic else headers

def _prepare_show_data(self):
"""Prepare headers and table data for output
Returns:
headers (list): Table headers
table (list): Table data
"""
table = []
headers = self._adjust_headers(self.headers)

for ns, stats in natsorted(self.data.items()):
if self.args.namespace is not None and self.args.namespace != ns:
continue
for name, values in natsorted(stats.items()):
if self.multi_asic.is_multi_asic:
row = [ns]
else:
row = []
row.extend([name, format_number_with_comma(values[0]), format_number_with_comma(values[1]), format_prate(values[2])])
table.append(row)

return headers, table

def _print_data(self, headers, table):
"""Print statistic data based on output format
Args:
headers (list): Table headers
table (list): Table data
"""
if self.args.json:
print(table_as_json(table, headers))
else:
print(tabulate(table, headers, tablefmt='simple', stralign='right'))

def clear(self):
"""Clear flow counter statistic. This function does not clear data from ASIC. Instead, it saves flow counter statistic to a file. When user
issue show command after clear, it does a diff between new data and saved data.
"""
self._collect()
self._save(self.data)
print('Flow Counters were successfully cleared')

@multi_asic_util.run_on_multi_asic
def _collect(self):
"""Collect flow counter statistic from DB. This function is called on a multi ASIC context.
"""
self.data.update(self._get_stats_from_db())

def _get_stats_from_db(self):
"""Get flow counter statistic from DB.
Returns:
dict: A dictionary. E.g: {<namespace>: {<trap_name>: [<value_in_pkts>, <value_in_bytes>, <rx_pps>, <counter_oid>]}}
"""
ns = self.multi_asic.current_namespace
name_map = self.db.get_all(self.db.COUNTERS_DB, self.name_map)
data = {ns: {}}
if not name_map:
return data

for name, counter_oid in name_map.items():
values = self._get_stats_value(counter_oid)

full_table_id = RATES_TABLE_PREFIX + counter_oid
counter_data = self.db.get(self.db.COUNTERS_DB, full_table_id, PPS_FIELD)
values.append(STATUS_NA if counter_data is None else counter_data)
values.append(counter_oid)
data[ns][name] = values
return data

def _get_stats_value(self, counter_oid):
"""Get statistic value from COUNTERS_DB COUNTERS table
Args:
counter_oid (string): OID of a generic counter
Returns:
values (list): A list of statistics value
"""
values = []
full_table_id = FLOW_COUNTER_TABLE_PREFIX + counter_oid
for field in flow_counters_fields:
counter_data = self.db.get(self.db.COUNTERS_DB, full_table_id, field)
values.append(STATUS_NA if counter_data is None else counter_data)
return values

def _save(self, data):
"""Save flow counter statistic to a file
"""
try:
if os.path.exists(self.data_file):
os.remove(self.data_file)

with open(self.data_file, 'wb') as f:
pickle.dump(data, f)
except IOError as e:
print('Failed to save statistic - {}'.format(repr(e)))

def _load(self):
"""Load flow counter statistic from a file
Returns:
dict: A dictionary. E.g: {<namespace>: {<trap_name>: [<value_in_pkts>, <value_in_bytes>, <rx_pps>, <counter_oid>]}}
"""
if not os.path.exists(self.data_file):
return None

try:
with open(self.data_file, 'rb') as f:
data = pickle.load(f)
except IOError as e:
print('Failed to load statistic - {}'.format(repr(e)))
return None

return data

def _diff(self, old_data, new_data):
"""Do a diff between new data and old data.
Args:
old_data (dict): E.g: {<namespace>: {<trap_name>: [<value_in_pkts>, <value_in_bytes>, <rx_pps>, <counter_oid>]}}
new_data (dict): E.g: {<namespace>: {<trap_name>: [<value_in_pkts>, <value_in_bytes>, <rx_pps>, <counter_oid>]}}
Returns:
bool: True if cache need to be updated
"""
if not old_data:
return False

need_update_cache = False
for ns, stats in new_data.items():
if ns not in old_data:
continue
old_stats = old_data[ns]
for name, values in stats.items():
if name not in old_stats:
continue

old_values = old_stats[name]
if values[-1] != old_values[-1]:
# Counter OID not equal means the trap was removed and added again. Removing a trap would cause
# the stats value restart from 0. To avoid get minus value here, it should not do diff in case
# counter OID is changed.
old_values[-1] = values[-1]
for i in diff_column_positions:
old_values[i] = 0
values[i] = ns_diff(values[i], old_values[i])
need_update_cache = True
continue

has_negative_diff = False
for i in diff_column_positions:
# If any diff has negative value, set all counter values to 0 and update cache
if values[i] < old_values[i]:
has_negative_diff = True
break

if has_negative_diff:
for i in diff_column_positions:
old_values[i] = 0
values[i] = ns_diff(values[i], old_values[i])
need_update_cache = True
continue

for i in diff_column_positions:
values[i] = ns_diff(values[i], old_values[i])

return need_update_cache


def main():
parser = argparse.ArgumentParser(description='Display the flow counters',
formatter_class=argparse.RawTextHelpFormatter,
epilog="""
Examples:
flow_counters_stat -c -t trap
flow_counters_stat -t trap
flow_counters_stat -d -t trap
""")
parser.add_argument('-c', '--clear', action='store_true', help='Copy & clear stats')
parser.add_argument('-d', '--delete', action='store_true', help='Delete saved stats')
parser.add_argument('-j', '--json', action='store_true', help='Display in JSON format')
parser.add_argument('-n','--namespace', default=None, help='Display flow counters for specific namespace')
parser.add_argument('-t', '--type', required=True, choices=['trap'],help='Flow counters type')

args = parser.parse_args()

stats = FlowCounterStats(args)
if args.clear:
stats.clear()
else:
stats.show()


if __name__ == '__main__':
main()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
'scripts/fast-reboot-dump.py',
'scripts/fdbclear',
'scripts/fdbshow',
'scripts/flow_counters_stat',
'scripts/gearboxutil',
'scripts/generate_dump',
'scripts/generate_shutdown_order.py',
Expand Down
Loading

0 comments on commit c05845d

Please sign in to comment.