Skip to content

Commit 3732ac5

Browse files
Add CLI for route flow counter feature (sonic-net#2031)
HLD: sonic-net/SONiC#908 Command reference : sonic-net/sonic-utilities#2069 - What I did Add CLIs for route flow counter feature - How I did it Add show command show flowcnt-route config and command group show flowcnt-route stats Add config command group config flowcnt-route pattern Add clear command group sonic-clear flowcnt-route - How to verify it 1. Full unit test cover 2. Manual test 3. sonic-mgmt test cases
1 parent 29771e7 commit 3732ac5

File tree

19 files changed

+1594
-18
lines changed

19 files changed

+1594
-18
lines changed

clear/main.py

+49
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import sys
55
import click
66
import utilities_common.cli as clicommon
7+
import utilities_common.multi_asic as multi_asic_util
78

9+
from flow_counter_util.route import exit_if_route_flow_counter_not_support
810
from utilities_common import util_base
911
from show.plugins.pbh import read_pbh_counters
1012
from config.plugins.pbh import serialize_pbh_counters
@@ -484,6 +486,53 @@ def flowcnt_trap():
484486
run_command(command)
485487

486488

489+
# ("sonic-clear flowcnt-route")
490+
@cli.group(invoke_without_command=True)
491+
@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')
492+
@click.pass_context
493+
def flowcnt_route(ctx, namespace):
494+
"""Clear all route flow counters"""
495+
exit_if_route_flow_counter_not_support()
496+
if ctx.invoked_subcommand is None:
497+
command = "flow_counters_stat -c -t route"
498+
# None namespace means default namespace
499+
if namespace is not None:
500+
command += " -n {}".format(namespace)
501+
clicommon.run_command(command)
502+
503+
504+
# ("sonic-clear flowcnt-route pattern")
505+
@flowcnt_route.command()
506+
@click.option('--vrf', help='VRF/VNET name or default VRF')
507+
@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')
508+
@click.argument('prefix-pattern', required=True)
509+
def pattern(prefix_pattern, vrf, namespace):
510+
"""Clear route flow counters by pattern"""
511+
command = "flow_counters_stat -c -t route --prefix_pattern {}".format(prefix_pattern)
512+
if vrf:
513+
command += ' --vrf {}'.format(vrf)
514+
# None namespace means default namespace
515+
if namespace is not None:
516+
command += " -n {}".format(namespace)
517+
clicommon.run_command(command)
518+
519+
520+
# ("sonic-clear flowcnt-route route")
521+
@flowcnt_route.command()
522+
@click.option('--vrf', help='VRF/VNET name or default VRF')
523+
@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')
524+
@click.argument('prefix', required=True)
525+
def route(prefix, vrf, namespace):
526+
"""Clear route flow counters by prefix"""
527+
command = "flow_counters_stat -c -t route --prefix {}".format(prefix)
528+
if vrf:
529+
command += ' --vrf {}'.format(vrf)
530+
# None namespace means default namespace
531+
if namespace is not None:
532+
command += " -n {}".format(namespace)
533+
clicommon.run_command(command)
534+
535+
487536
# Load plugins and register them
488537
helper = util_base.UtilHelper()
489538
helper.load_and_register_plugins(plugins, cli)

config/flow_counters.py

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import click
2+
import ipaddress
3+
4+
from flow_counter_util.route import FLOW_COUNTER_ROUTE_PATTERN_TABLE, FLOW_COUNTER_ROUTE_MAX_MATCH_FIELD, DEFAULT_VRF, PATTERN_SEPARATOR
5+
from flow_counter_util.route import build_route_pattern, extract_route_pattern, exit_if_route_flow_counter_not_support
6+
from utilities_common.cli import AbbreviationGroup, pass_db
7+
from utilities_common import cli # To make mock work in unit test
8+
9+
#
10+
# 'flowcnt-route' group ('config flowcnt-route ...')
11+
#
12+
13+
14+
@click.group(cls=AbbreviationGroup, invoke_without_command=False)
15+
def flowcnt_route():
16+
"""Route flow counter related configuration tasks"""
17+
pass
18+
19+
20+
@flowcnt_route.group()
21+
def pattern():
22+
"""Set pattern for route flow counter"""
23+
pass
24+
25+
26+
@pattern.command(name='add')
27+
@click.option('-y', '--yes', is_flag=True)
28+
@click.option('--vrf', help='VRF/VNET name or default VRF')
29+
@click.option('--max', 'max_allowed_match', type=click.IntRange(1, 50), default=30, show_default=True, help='Max allowed match count')
30+
@click.argument('prefix-pattern', required=True)
31+
@pass_db
32+
def pattern_add(db, yes, vrf, max_allowed_match, prefix_pattern):
33+
"""Add pattern for route flow counter"""
34+
_update_route_flow_counter_config(db, vrf, max_allowed_match, prefix_pattern, True, yes)
35+
36+
37+
@pattern.command(name='remove')
38+
@click.option('--vrf', help='VRF/VNET name or default VRF')
39+
@click.argument('prefix-pattern', required=True)
40+
@pass_db
41+
def pattern_remove(db, vrf, prefix_pattern):
42+
"""Remove pattern for route flow counter"""
43+
_update_route_flow_counter_config(db, vrf, None, prefix_pattern, False)
44+
45+
46+
def _update_route_flow_counter_config(db, vrf, max_allowed_match, prefix_pattern, add, yes=False):
47+
"""
48+
Update route flow counter config
49+
:param db: db object
50+
:param vrf: vrf string, empty vrf will be treated as default vrf
51+
:param max_allowed_match: max allowed match count, $FLOW_COUNTER_ROUTE_MAX_MATCH_FIELD will be used if not specified
52+
:param prefix_pattern: route prefix pattern, automatically add prefix length if not specified
53+
:param add: True to add/set the configuration, otherwise remove
54+
:param yes: Don't ask question if True
55+
:return:
56+
"""
57+
exit_if_route_flow_counter_not_support()
58+
59+
if add:
60+
try:
61+
net = ipaddress.ip_network(prefix_pattern, strict=False)
62+
except ValueError as e:
63+
click.echo('Invalid prefix pattern: {}'.format(prefix_pattern))
64+
exit(1)
65+
66+
if '/' not in prefix_pattern:
67+
prefix_pattern += '/' + str(net.prefixlen)
68+
69+
key = build_route_pattern(vrf, prefix_pattern)
70+
for _, cfgdb in db.cfgdb_clients.items():
71+
if _try_find_existing_pattern_by_ip_type(cfgdb, net, key, yes):
72+
entry_data = cfgdb.get_entry(FLOW_COUNTER_ROUTE_PATTERN_TABLE, key)
73+
old_max_allowed_match = entry_data.get(FLOW_COUNTER_ROUTE_MAX_MATCH_FIELD)
74+
if old_max_allowed_match is not None and int(old_max_allowed_match) == max_allowed_match:
75+
click.echo('The route pattern already exists, nothing to be changed')
76+
exit(1)
77+
cfgdb.mod_entry(FLOW_COUNTER_ROUTE_PATTERN_TABLE,
78+
key,
79+
{FLOW_COUNTER_ROUTE_MAX_MATCH_FIELD: str(max_allowed_match)})
80+
else:
81+
found = False
82+
key = build_route_pattern(vrf, prefix_pattern)
83+
for _, cfgdb in db.cfgdb_clients.items():
84+
pattern_table = cfgdb.get_table(FLOW_COUNTER_ROUTE_PATTERN_TABLE)
85+
86+
for existing_key in pattern_table:
87+
exist_vrf, existing_prefix = extract_route_pattern(existing_key)
88+
if (exist_vrf == vrf or (vrf is None and exist_vrf == DEFAULT_VRF)) and existing_prefix == prefix_pattern:
89+
found = True
90+
cfgdb.set_entry(FLOW_COUNTER_ROUTE_PATTERN_TABLE, key, None)
91+
if not found:
92+
click.echo("Failed to remove route pattern: {} does not exist".format(key))
93+
exit(1)
94+
95+
96+
def _try_find_existing_pattern_by_ip_type(cfgdb, input_net, input_key, yes):
97+
"""Try to find the same IP type pattern from CONFIG DB.
98+
1. If found a pattern with the same IP type, but the patter does not equal, ask user if need to replace the old with new one
99+
a. If user types "yes", remove the old one, return False
100+
b. If user types "no", exit
101+
2. If found a pattern with the same IP type and the pattern equal, return True
102+
3. If not found a pattern with the same IP type, return False
103+
104+
Args:
105+
cfgdb (object): CONFIG DB object
106+
input_net (object): Input ip_network object
107+
input_key (str): Input key
108+
yes (bool): Whether ask user question
109+
110+
Returns:
111+
bool: True if found the same pattern in CONFIG DB
112+
"""
113+
input_type = type(input_net) # IPv4 or IPv6
114+
found_invalid = []
115+
found = None
116+
pattern_table = cfgdb.get_table(FLOW_COUNTER_ROUTE_PATTERN_TABLE)
117+
for existing_key in pattern_table:
118+
if isinstance(existing_key, tuple):
119+
existing_prefix = existing_key[1]
120+
existing_key = PATTERN_SEPARATOR.join(existing_key)
121+
else:
122+
_, existing_prefix = extract_route_pattern(existing_key)
123+
124+
# In case user configures an invalid pattern via CONFIG DB.
125+
if not existing_prefix: # Invalid pattern such as: "vrf1|"
126+
click.echo('Detect invalid route pattern in existing configuration {}'.format(existing_key))
127+
found_invalid.append(existing_key)
128+
continue
129+
130+
try:
131+
existing_net = ipaddress.ip_network(existing_prefix, strict=False)
132+
except ValueError as e: # Invalid pattern such as: "vrf1|invalid"
133+
click.echo('Detect invalid route pattern in existing configuration {}'.format(existing_key))
134+
found_invalid.append(existing_key)
135+
continue
136+
137+
if type(existing_net) == input_type:
138+
found = existing_key
139+
break
140+
141+
if found == input_key:
142+
return True
143+
144+
if not found and found_invalid:
145+
# If not found but there is an invalid one, ask user to replace the invalid one
146+
found = found_invalid[0]
147+
148+
if found:
149+
if not yes:
150+
answer = cli.query_yes_no('Only support 1 IPv4 route pattern and 1 IPv6 route pattern, remove existing pattern {}?'.format(found))
151+
else:
152+
answer = True
153+
if answer:
154+
click.echo('Replacing existing route pattern {} with {}'.format(existing_key, input_key))
155+
cfgdb.set_entry(FLOW_COUNTER_ROUTE_PATTERN_TABLE, existing_key, None)
156+
else:
157+
exit(0)
158+
return False

config/main.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from . import chassis_modules
3636
from . import console
3737
from . import feature
38+
from . import flow_counters
3839
from . import kdump
3940
from . import kube
4041
from . import muxcable
@@ -789,7 +790,7 @@ def _per_namespace_swss_ready(service_name):
789790
return False
790791

791792
def _swss_ready():
792-
list_of_swss = []
793+
list_of_swss = []
793794
num_asics = multi_asic.get_num_asics()
794795
if num_asics == 1:
795796
list_of_swss.append("swss.service")
@@ -802,7 +803,7 @@ def _swss_ready():
802803
if _per_namespace_swss_ready(service_name) == False:
803804
return False
804805

805-
return True
806+
return True
806807

807808
def _is_system_starting():
808809
out = clicommon.run_command("sudo systemctl is-system-running", return_cmd=True)
@@ -1076,6 +1077,7 @@ def config(ctx):
10761077
config.add_command(chassis_modules.chassis)
10771078
config.add_command(console.console)
10781079
config.add_command(feature.feature)
1080+
config.add_command(flow_counters.flowcnt_route)
10791081
config.add_command(kdump.kdump)
10801082
config.add_command(kube.kubernetes)
10811083
config.add_command(muxcable.muxcable)
@@ -1482,10 +1484,10 @@ def reload(db, filename, yes, load_sysinfo, no_service_restart, disable_arp_cach
14821484

14831485

14841486
config_gen_opts = ""
1485-
1487+
14861488
if os.path.isfile(INIT_CFG_FILE):
14871489
config_gen_opts += " -j {} ".format(INIT_CFG_FILE)
1488-
1490+
14891491
if file_format == 'config_db':
14901492
config_gen_opts += ' -j {} '.format(file)
14911493
else:
@@ -6239,7 +6241,7 @@ def del_subinterface(ctx, subinterface_name):
62396241
sub_intfs = [k for k,v in subintf_config_db.items() if type(k) != tuple]
62406242
if subinterface_name not in sub_intfs:
62416243
ctx.fail("{} does not exists".format(subinterface_name))
6242-
6244+
62436245
ips = {}
62446246
ips = [ k[1] for k in config_db.get_table('VLAN_SUB_INTERFACE') if type(k) == tuple and k[0] == subinterface_name ]
62456247
for ip in ips:

counterpoll/main.py

+39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import click
22
import json
3+
from flow_counter_util.route import exit_if_route_flow_counter_not_support
34
from swsscommon.swsscommon import ConfigDBConnector
45
from tabulate import tabulate
56

@@ -347,6 +348,40 @@ def disable(ctx):
347348
fc_info['FLEX_COUNTER_STATUS'] = 'disable'
348349
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_TRAP", fc_info)
349350

351+
# Route flow counter commands
352+
@cli.group()
353+
@click.pass_context
354+
def flowcnt_route(ctx):
355+
""" Route flow counter commands """
356+
exit_if_route_flow_counter_not_support()
357+
ctx.obj = ConfigDBConnector()
358+
ctx.obj.connect()
359+
360+
@flowcnt_route.command()
361+
@click.argument('poll_interval', type=click.IntRange(1000, 30000))
362+
@click.pass_context
363+
def interval(ctx, poll_interval):
364+
""" Set route flow counter query interval """
365+
fc_info = {}
366+
fc_info['POLL_INTERVAL'] = poll_interval
367+
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_ROUTE", fc_info)
368+
369+
@flowcnt_route.command()
370+
@click.pass_context
371+
def enable(ctx):
372+
""" Enable route flow counter query """
373+
fc_info = {}
374+
fc_info['FLEX_COUNTER_STATUS'] = 'enable'
375+
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_ROUTE", fc_info)
376+
377+
@flowcnt_route.command()
378+
@click.pass_context
379+
def disable(ctx):
380+
""" Disable route flow counter query """
381+
fc_info = {}
382+
fc_info['FLEX_COUNTER_STATUS'] = 'disable'
383+
ctx.obj.mod_entry("FLEX_COUNTER_TABLE", "FLOW_CNT_ROUTE", fc_info)
384+
350385
@cli.command()
351386
def show():
352387
""" Show the counter configuration """
@@ -363,6 +398,7 @@ def show():
363398
acl_info = configdb.get_entry('FLEX_COUNTER_TABLE', ACL)
364399
tunnel_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'TUNNEL')
365400
trap_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'FLOW_CNT_TRAP')
401+
route_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'FLOW_CNT_ROUTE')
366402

367403
header = ("Type", "Interval (in ms)", "Status")
368404
data = []
@@ -388,6 +424,9 @@ def show():
388424
data.append(["TUNNEL_STAT", rif_info.get("POLL_INTERVAL", DEFLT_10_SEC), rif_info.get("FLEX_COUNTER_STATUS", DISABLE)])
389425
if trap_info:
390426
data.append(["FLOW_CNT_TRAP_STAT", trap_info.get("POLL_INTERVAL", DEFLT_10_SEC), trap_info.get("FLEX_COUNTER_STATUS", DISABLE)])
427+
if route_info:
428+
data.append(["FLOW_CNT_ROUTE_STAT", route_info.get("POLL_INTERVAL", DEFLT_10_SEC),
429+
route_info.get("FLEX_COUNTER_STATUS", DISABLE)])
391430

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

flow_counter_util/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)