Skip to content

Commit a35423e

Browse files
Add trap flow counter support
1 parent c0b9917 commit a35423e

File tree

13 files changed

+528
-3
lines changed

13 files changed

+528
-3
lines changed

clear/main.py

+8
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,14 @@ def statistics(db):
476476
def remap_keys(dict):
477477
return [{'key': k, 'value': v} for k, v in dict.items()]
478478

479+
# ("sonic-clear flowcnt-trap")
480+
@cli.command()
481+
def flowcnt_trap():
482+
""" Clear trap flow counters """
483+
command = "flow_counters_stat -c -t trap"
484+
run_command(command)
485+
486+
479487
# Load plugins and register them
480488
helper = util_base.UtilHelper()
481489
for plugin in helper.load_plugins(plugins):

counterpoll/main.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ def disable():
5353
# Port counter commands
5454
@cli.group()
5555
def port():
56-
""" Queue counter commands """
56+
""" Port counter commands """
5757

5858
@port.command()
5959
@click.argument('poll_interval', type=click.IntRange(100, 30000))
6060
def interval(poll_interval):
61-
""" Set queue counter query interval """
61+
""" Set port counter query interval """
6262
configdb = ConfigDBConnector()
6363
configdb.connect()
6464
port_info = {}
@@ -241,6 +241,39 @@ def disable():
241241
configdb.mod_entry("FLEX_COUNTER_TABLE", "PG_WATERMARK", fc_info)
242242
configdb.mod_entry("FLEX_COUNTER_TABLE", BUFFER_POOL_WATERMARK, fc_info)
243243

244+
# Trap flow counter commands
245+
@cli.group()
246+
@click.pass_context
247+
def flowcnt_trap(ctx):
248+
""" Trap flow counter commands """
249+
ctx.obj = ConfigDBConnector()
250+
ctx.obj.connect()
251+
252+
@flowcnt_trap.command()
253+
@click.argument('poll_interval', type=click.IntRange(1000, 30000))
254+
@click.pass_context
255+
def interval(ctx, poll_interval):
256+
""" Set trap flow counter query interval """
257+
fc_info = {}
258+
fc_info['POLL_INTERVAL'] = poll_interval
259+
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_TRAP", fc_info)
260+
261+
@flowcnt_trap.command()
262+
@click.pass_context
263+
def enable(ctx):
264+
""" Enable trap flow counter query """
265+
fc_info = {}
266+
fc_info['FLEX_COUNTER_STATUS'] = 'enable'
267+
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_TRAP", fc_info)
268+
269+
@flowcnt_trap.command()
270+
@click.pass_context
271+
def disable(ctx):
272+
""" Disable trap flow counter query """
273+
fc_info = {}
274+
fc_info['FLEX_COUNTER_STATUS'] = 'disable'
275+
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_TRAP", fc_info)
276+
244277
@cli.command()
245278
def show():
246279
""" Show the counter configuration """
@@ -254,6 +287,7 @@ def show():
254287
pg_wm_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'PG_WATERMARK')
255288
pg_drop_info = configdb.get_entry('FLEX_COUNTER_TABLE', PG_DROP)
256289
buffer_pool_wm_info = configdb.get_entry('FLEX_COUNTER_TABLE', BUFFER_POOL_WATERMARK)
290+
trap_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'FLOW_CNT_TRAP')
257291

258292
header = ("Type", "Interval (in ms)", "Status")
259293
data = []
@@ -273,6 +307,8 @@ def show():
273307
data.append(['PG_DROP_STAT', pg_drop_info.get("POLL_INTERVAL", DEFLT_10_SEC), pg_drop_info.get("FLEX_COUNTER_STATUS", DISABLE)])
274308
if buffer_pool_wm_info:
275309
data.append(["BUFFER_POOL_WATERMARK_STAT", buffer_pool_wm_info.get("POLL_INTERVAL", DEFLT_10_SEC), buffer_pool_wm_info.get("FLEX_COUNTER_STATUS", DISABLE)])
310+
if trap_info:
311+
data.append(["FLOW_CNT_TRAP_STAT", trap_info.get("POLL_INTERVAL", DEFLT_1_SEC), trap_info.get("FLEX_COUNTER_STATUS", DISABLE)])
276312

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

scripts/flow_counters_stat

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import os
5+
import _pickle as pickle
6+
import sys
7+
8+
from natsort import natsorted
9+
from tabulate import tabulate
10+
11+
# mock the redis for unit test purposes #
12+
try:
13+
if os.environ["UTILITIES_UNIT_TESTING"] == "2":
14+
modules_path = os.path.join(os.path.dirname(__file__), "..")
15+
tests_path = os.path.join(modules_path, "tests")
16+
sys.path.insert(0, modules_path)
17+
sys.path.insert(0, tests_path)
18+
import mock_tables.dbconnector
19+
if os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] == "multi_asic":
20+
import mock_tables.mock_multi_asic
21+
mock_tables.dbconnector.load_namespace_config()
22+
23+
except KeyError:
24+
pass
25+
26+
import utilities_common.multi_asic as multi_asic_util
27+
from utilities_common.netstat import format_number_with_comma, table_as_json, ns_diff, format_prate
28+
29+
# Flow counter meta data, new type of flow counters can extend this dictinary to reuse existing logic
30+
flow_counter_meta = {
31+
'trap': {
32+
'headers': ['Trap Name', 'Packets', 'Bytes', 'PPS'],
33+
'name_map': 'COUNTERS_TRAP_NAME_MAP',
34+
}
35+
}
36+
flow_counters_fields = ['SAI_COUNTER_STAT_PACKETS', 'SAI_COUNTER_STAT_BYTES']
37+
38+
# Only do diff for 'Packets' and 'Bytes'
39+
diff_column_positions = set([0, 1])
40+
41+
FLOW_COUNTER_TABLE_PREFIX = "COUNTERS:"
42+
RATES_TABLE_PREFIX = 'RATES:'
43+
PPS_FIELD = 'RX_PPS'
44+
STATUS_NA = 'N/A'
45+
46+
47+
class FlowCounterStats(object):
48+
def __init__(self, args):
49+
self.db = None
50+
self.multi_asic = multi_asic_util.MultiAsic(namespace_option=args.namespace)
51+
self.args = args
52+
meta_data = flow_counter_meta[args.type]
53+
self.name_map = meta_data['name_map']
54+
self.headers = meta_data['headers']
55+
self.data_file = os.path.join('/tmp/{}-stats-{}'.format(args.type, os.getuid()))
56+
if self.args.delete and os.path.exists(self.data_file):
57+
os.remove(self.data_file)
58+
self.data = {}
59+
60+
def show(self):
61+
"""Show flow counter statistic
62+
"""
63+
self._collect()
64+
old_data = self._load()
65+
self._diff(old_data, self.data)
66+
67+
table = []
68+
if self.multi_asic.is_multi_asic:
69+
# On multi ASIC platform, an extra column "ASIC ID" must be present to avoid duplicate entry name
70+
headers = ['ASIC ID'] + self.headers
71+
for ns, stats in natsorted(self.data.items()):
72+
for name, values in natsorted(stats.items()):
73+
row = [ns, name, format_number_with_comma(values[0]), format_number_with_comma(values[1]), format_prate(values[2])]
74+
table.append(row)
75+
else:
76+
headers = self.headers
77+
for ns, stats in natsorted(self.data.items()):
78+
for name, values in natsorted(stats.items()):
79+
row = [name, format_number_with_comma(values[0]), format_number_with_comma(values[1]), format_prate(values[2])]
80+
table.append(row)
81+
if self.args.json:
82+
print(table_as_json(table, headers))
83+
else:
84+
print(tabulate(table, headers, tablefmt='simple', stralign='right'))
85+
86+
def clear(self):
87+
"""Clear flow counter statistic. This function does not clear data from ASIC. Instead, it saves flow counter statistic to a file. When user
88+
issue show command after clear, it does a diff between new data and saved data.
89+
"""
90+
self._collect()
91+
self._save()
92+
print('Flow Counters were successfully cleared')
93+
94+
@multi_asic_util.run_on_multi_asic
95+
def _collect(self):
96+
"""Collect flow counter statistic from DB. This function is called on a multi ASIC context.
97+
"""
98+
self.data.update(self._get_stats_from_db())
99+
100+
def _get_stats_from_db(self):
101+
"""Get flow counter statistic from DB.
102+
103+
Returns:
104+
dict: A dictionary. E.g: {<namespace>: {<trap_name>: [<value_in_pkts>, <value_in_bytes>, <rx_pps>]}}
105+
"""
106+
ns = self.multi_asic.current_namespace
107+
name_map = self.db.get_all(self.db.COUNTERS_DB, self.name_map)
108+
data = {ns: {}}
109+
if not name_map:
110+
return data
111+
112+
for name, counter_oid in name_map.items():
113+
values = []
114+
full_table_id = FLOW_COUNTER_TABLE_PREFIX + counter_oid
115+
for field in flow_counters_fields:
116+
counter_data = self.db.get(self.db.COUNTERS_DB, full_table_id, field)
117+
values.append(STATUS_NA if counter_data is None else counter_data)
118+
119+
full_table_id = RATES_TABLE_PREFIX + counter_oid
120+
counter_data = self.db.get(self.db.COUNTERS_DB, full_table_id, PPS_FIELD)
121+
values.append(STATUS_NA if counter_data is None else counter_data)
122+
values.append(counter_oid)
123+
data[ns][name] = values
124+
return data
125+
126+
def _save(self):
127+
"""Save flow counter statistic to a file
128+
"""
129+
try:
130+
if os.path.exists(self.data_file):
131+
os.remove(self.data_file)
132+
133+
with open(self.data_file, 'wb') as f:
134+
pickle.dump(self.data, f)
135+
except IOError as e:
136+
print('Failed to save statistic - {}'.format(repr(e)))
137+
138+
def _load(self):
139+
"""Load flow counter statistic from a file
140+
141+
Returns:
142+
dict: A dictionary. E.g: {<namespace>: {<trap_name>: [<value_in_pkts>, <value_in_bytes>, <rx_pps>]}}
143+
"""
144+
if not os.path.exists(self.data_file):
145+
return None
146+
147+
try:
148+
with open(self.data_file, 'rb') as f:
149+
data = pickle.load(f)
150+
except IOError as e:
151+
print('Failed to load statistic - {}'.format(repr(e)))
152+
return None
153+
154+
return data
155+
156+
def _diff(self, old_data, new_data):
157+
"""Do a diff between new data and old data.
158+
159+
Args:
160+
old_data (dict): E.g: {<namespace>: {<trap_name>: [<value_in_pkts>, <value_in_bytes>, <rx_pps>]}}
161+
new_data (dict): E.g: {<namespace>: {<trap_name>: [<value_in_pkts>, <value_in_bytes>, <rx_pps>]}}
162+
"""
163+
if not old_data:
164+
return
165+
166+
for ns, stats in new_data.items():
167+
if ns not in old_data:
168+
continue
169+
old_stats = old_data[ns]
170+
for name, values in stats.items():
171+
if name not in old_stats:
172+
continue
173+
174+
old_values = old_stats[name]
175+
if values[-1] != old_values[-1]:
176+
# Counter OID not equal means the trap was removed and added again. Removing a trap would cause
177+
# the stats value restart from 0. To avoid get minus value here, it should not do diff in case
178+
# counter OID is changed.
179+
continue
180+
181+
for i in diff_column_positions:
182+
values[i] = ns_diff(values[i], old_values[i])
183+
184+
185+
def main():
186+
parser = argparse.ArgumentParser(description='Display the flow counters',
187+
formatter_class=argparse.RawTextHelpFormatter,
188+
epilog="""
189+
Examples:
190+
flow_counters_stat -c -t trap
191+
flow_counters_stat -t trap
192+
flow_counters_stat -d -t trap
193+
""")
194+
parser.add_argument('-c', '--clear', action='store_true', help='Copy & clear stats')
195+
parser.add_argument('-d', '--delete', action='store_true', help='Delete saved stats')
196+
parser.add_argument('-j', '--json', action='store_true', help='Display in JSON format')
197+
parser.add_argument('-n','--namespace', default=None, help='Display flow counters for specific namespace')
198+
parser.add_argument('-t', '--type', required=True, choices=['trap'],help='Flow counters type')
199+
200+
args = parser.parse_args()
201+
202+
stats = FlowCounterStats(args)
203+
if args.clear:
204+
stats.clear()
205+
else:
206+
stats.show()
207+
208+
209+
if __name__ == '__main__':
210+
main()

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
'scripts/fast-reboot-dump.py',
9797
'scripts/fdbclear',
9898
'scripts/fdbshow',
99+
'scripts/flow_counters_stat',
99100
'scripts/gearboxutil',
100101
'scripts/generate_dump',
101102
'scripts/generate_shutdown_order.py',

show/flow_counters.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import click
2+
import utilities_common.cli as clicommon
3+
import utilities_common.multi_asic as multi_asic_util
4+
5+
#
6+
# 'flowcnt-trap' group ###
7+
#
8+
9+
@click.group(cls=clicommon.AliasedGroup)
10+
def flowcnt_trap():
11+
"""Show trap flow counter related information"""
12+
pass
13+
14+
@flowcnt_trap.command()
15+
@click.option('--verbose', is_flag=True, help="Enable verbose output")
16+
@click.option('--namespace', '-n', 'namespace', default=None, type=click.Choice(multi_asic_util.multi_asic_ns_choices()), show_default=True, help='Namespace name or all')
17+
def stats(verbose, namespace):
18+
"""Show trap flow counter statistic"""
19+
cmd = "flow_counters_stat -t trap"
20+
if namespace is not None:
21+
cmd += " -n {}".format(namespace)
22+
clicommon.run_command(cmd, display_cmd=verbose)

show/main.py

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from . import dropcounters
4141
from . import feature
4242
from . import fgnhg
43+
from . import flow_counters
4344
from . import gearbox
4445
from . import interfaces
4546
from . import kdump
@@ -179,6 +180,7 @@ def cli(ctx):
179180
cli.add_command(dropcounters.dropcounters)
180181
cli.add_command(feature.feature)
181182
cli.add_command(fgnhg.fgnhg)
183+
cli.add_command(flow_counters.flowcnt_trap)
182184
cli.add_command(kdump.kdump)
183185
cli.add_command(interfaces.interfaces)
184186
cli.add_command(kdump.kdump)

tests/counterpoll_input/config_db.json

+3
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,9 @@
784784
},
785785
"PORT": {
786786
"FLEX_COUNTER_STATUS": "enable"
787+
},
788+
"FLOW_CNT_TRAP": {
789+
"FLEX_COUNTER_STATUS": "enable"
787790
}
788791
},
789792
"PORT": {

0 commit comments

Comments
 (0)