Skip to content
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

Add trap flow counter support #1868

Merged
merged 4 commits into from
Nov 25, 2021
Merged
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
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good logic to handle. However, what if OID returned is the same as before delete? Should we rather not check if the diff is negative and consider it as a case where trap got deleted?

# 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