Skip to content

Commit

Permalink
Add CLI for route flow counter feature (#2031)
Browse files Browse the repository at this point in the history
HLD: sonic-net/SONiC#908
Command reference : #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
  • Loading branch information
Junchao-Mellanox authored Apr 18, 2022
1 parent 29771e7 commit 3732ac5
Show file tree
Hide file tree
Showing 19 changed files with 1,594 additions and 18 deletions.
49 changes: 49 additions & 0 deletions clear/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import sys
import click
import utilities_common.cli as clicommon
import utilities_common.multi_asic as multi_asic_util

from flow_counter_util.route import exit_if_route_flow_counter_not_support
from utilities_common import util_base
from show.plugins.pbh import read_pbh_counters
from config.plugins.pbh import serialize_pbh_counters
Expand Down Expand Up @@ -484,6 +486,53 @@ def flowcnt_trap():
run_command(command)


# ("sonic-clear flowcnt-route")
@cli.group(invoke_without_command=True)
@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')
@click.pass_context
def flowcnt_route(ctx, namespace):
"""Clear all route flow counters"""
exit_if_route_flow_counter_not_support()
if ctx.invoked_subcommand is None:
command = "flow_counters_stat -c -t route"
# None namespace means default namespace
if namespace is not None:
command += " -n {}".format(namespace)
clicommon.run_command(command)


# ("sonic-clear flowcnt-route pattern")
@flowcnt_route.command()
@click.option('--vrf', help='VRF/VNET name or default VRF')
@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')
@click.argument('prefix-pattern', required=True)
def pattern(prefix_pattern, vrf, namespace):
"""Clear route flow counters by pattern"""
command = "flow_counters_stat -c -t route --prefix_pattern {}".format(prefix_pattern)
if vrf:
command += ' --vrf {}'.format(vrf)
# None namespace means default namespace
if namespace is not None:
command += " -n {}".format(namespace)
clicommon.run_command(command)


# ("sonic-clear flowcnt-route route")
@flowcnt_route.command()
@click.option('--vrf', help='VRF/VNET name or default VRF')
@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')
@click.argument('prefix', required=True)
def route(prefix, vrf, namespace):
"""Clear route flow counters by prefix"""
command = "flow_counters_stat -c -t route --prefix {}".format(prefix)
if vrf:
command += ' --vrf {}'.format(vrf)
# None namespace means default namespace
if namespace is not None:
command += " -n {}".format(namespace)
clicommon.run_command(command)


# Load plugins and register them
helper = util_base.UtilHelper()
helper.load_and_register_plugins(plugins, cli)
Expand Down
158 changes: 158 additions & 0 deletions config/flow_counters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import click
import ipaddress

from flow_counter_util.route import FLOW_COUNTER_ROUTE_PATTERN_TABLE, FLOW_COUNTER_ROUTE_MAX_MATCH_FIELD, DEFAULT_VRF, PATTERN_SEPARATOR
from flow_counter_util.route import build_route_pattern, extract_route_pattern, exit_if_route_flow_counter_not_support
from utilities_common.cli import AbbreviationGroup, pass_db
from utilities_common import cli # To make mock work in unit test

#
# 'flowcnt-route' group ('config flowcnt-route ...')
#


@click.group(cls=AbbreviationGroup, invoke_without_command=False)
def flowcnt_route():
"""Route flow counter related configuration tasks"""
pass


@flowcnt_route.group()
def pattern():
"""Set pattern for route flow counter"""
pass


@pattern.command(name='add')
@click.option('-y', '--yes', is_flag=True)
@click.option('--vrf', help='VRF/VNET name or default VRF')
@click.option('--max', 'max_allowed_match', type=click.IntRange(1, 50), default=30, show_default=True, help='Max allowed match count')
@click.argument('prefix-pattern', required=True)
@pass_db
def pattern_add(db, yes, vrf, max_allowed_match, prefix_pattern):
"""Add pattern for route flow counter"""
_update_route_flow_counter_config(db, vrf, max_allowed_match, prefix_pattern, True, yes)


@pattern.command(name='remove')
@click.option('--vrf', help='VRF/VNET name or default VRF')
@click.argument('prefix-pattern', required=True)
@pass_db
def pattern_remove(db, vrf, prefix_pattern):
"""Remove pattern for route flow counter"""
_update_route_flow_counter_config(db, vrf, None, prefix_pattern, False)


def _update_route_flow_counter_config(db, vrf, max_allowed_match, prefix_pattern, add, yes=False):
"""
Update route flow counter config
:param db: db object
:param vrf: vrf string, empty vrf will be treated as default vrf
:param max_allowed_match: max allowed match count, $FLOW_COUNTER_ROUTE_MAX_MATCH_FIELD will be used if not specified
:param prefix_pattern: route prefix pattern, automatically add prefix length if not specified
:param add: True to add/set the configuration, otherwise remove
:param yes: Don't ask question if True
:return:
"""
exit_if_route_flow_counter_not_support()

if add:
try:
net = ipaddress.ip_network(prefix_pattern, strict=False)
except ValueError as e:
click.echo('Invalid prefix pattern: {}'.format(prefix_pattern))
exit(1)

if '/' not in prefix_pattern:
prefix_pattern += '/' + str(net.prefixlen)

key = build_route_pattern(vrf, prefix_pattern)
for _, cfgdb in db.cfgdb_clients.items():
if _try_find_existing_pattern_by_ip_type(cfgdb, net, key, yes):
entry_data = cfgdb.get_entry(FLOW_COUNTER_ROUTE_PATTERN_TABLE, key)
old_max_allowed_match = entry_data.get(FLOW_COUNTER_ROUTE_MAX_MATCH_FIELD)
if old_max_allowed_match is not None and int(old_max_allowed_match) == max_allowed_match:
click.echo('The route pattern already exists, nothing to be changed')
exit(1)
cfgdb.mod_entry(FLOW_COUNTER_ROUTE_PATTERN_TABLE,
key,
{FLOW_COUNTER_ROUTE_MAX_MATCH_FIELD: str(max_allowed_match)})
else:
found = False
key = build_route_pattern(vrf, prefix_pattern)
for _, cfgdb in db.cfgdb_clients.items():
pattern_table = cfgdb.get_table(FLOW_COUNTER_ROUTE_PATTERN_TABLE)

for existing_key in pattern_table:
exist_vrf, existing_prefix = extract_route_pattern(existing_key)
if (exist_vrf == vrf or (vrf is None and exist_vrf == DEFAULT_VRF)) and existing_prefix == prefix_pattern:
found = True
cfgdb.set_entry(FLOW_COUNTER_ROUTE_PATTERN_TABLE, key, None)
if not found:
click.echo("Failed to remove route pattern: {} does not exist".format(key))
exit(1)


def _try_find_existing_pattern_by_ip_type(cfgdb, input_net, input_key, yes):
"""Try to find the same IP type pattern from CONFIG DB.
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
a. If user types "yes", remove the old one, return False
b. If user types "no", exit
2. If found a pattern with the same IP type and the pattern equal, return True
3. If not found a pattern with the same IP type, return False
Args:
cfgdb (object): CONFIG DB object
input_net (object): Input ip_network object
input_key (str): Input key
yes (bool): Whether ask user question
Returns:
bool: True if found the same pattern in CONFIG DB
"""
input_type = type(input_net) # IPv4 or IPv6
found_invalid = []
found = None
pattern_table = cfgdb.get_table(FLOW_COUNTER_ROUTE_PATTERN_TABLE)
for existing_key in pattern_table:
if isinstance(existing_key, tuple):
existing_prefix = existing_key[1]
existing_key = PATTERN_SEPARATOR.join(existing_key)
else:
_, existing_prefix = extract_route_pattern(existing_key)

# In case user configures an invalid pattern via CONFIG DB.
if not existing_prefix: # Invalid pattern such as: "vrf1|"
click.echo('Detect invalid route pattern in existing configuration {}'.format(existing_key))
found_invalid.append(existing_key)
continue

try:
existing_net = ipaddress.ip_network(existing_prefix, strict=False)
except ValueError as e: # Invalid pattern such as: "vrf1|invalid"
click.echo('Detect invalid route pattern in existing configuration {}'.format(existing_key))
found_invalid.append(existing_key)
continue

if type(existing_net) == input_type:
found = existing_key
break

if found == input_key:
return True

if not found and found_invalid:
# If not found but there is an invalid one, ask user to replace the invalid one
found = found_invalid[0]

if found:
if not yes:
answer = cli.query_yes_no('Only support 1 IPv4 route pattern and 1 IPv6 route pattern, remove existing pattern {}?'.format(found))
else:
answer = True
if answer:
click.echo('Replacing existing route pattern {} with {}'.format(existing_key, input_key))
cfgdb.set_entry(FLOW_COUNTER_ROUTE_PATTERN_TABLE, existing_key, None)
else:
exit(0)
return False
12 changes: 7 additions & 5 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from . import chassis_modules
from . import console
from . import feature
from . import flow_counters
from . import kdump
from . import kube
from . import muxcable
Expand Down Expand Up @@ -789,7 +790,7 @@ def _per_namespace_swss_ready(service_name):
return False

def _swss_ready():
list_of_swss = []
list_of_swss = []
num_asics = multi_asic.get_num_asics()
if num_asics == 1:
list_of_swss.append("swss.service")
Expand All @@ -802,7 +803,7 @@ def _swss_ready():
if _per_namespace_swss_ready(service_name) == False:
return False

return True
return True

def _is_system_starting():
out = clicommon.run_command("sudo systemctl is-system-running", return_cmd=True)
Expand Down Expand Up @@ -1076,6 +1077,7 @@ def config(ctx):
config.add_command(chassis_modules.chassis)
config.add_command(console.console)
config.add_command(feature.feature)
config.add_command(flow_counters.flowcnt_route)
config.add_command(kdump.kdump)
config.add_command(kube.kubernetes)
config.add_command(muxcable.muxcable)
Expand Down Expand Up @@ -1482,10 +1484,10 @@ def reload(db, filename, yes, load_sysinfo, no_service_restart, disable_arp_cach


config_gen_opts = ""

if os.path.isfile(INIT_CFG_FILE):
config_gen_opts += " -j {} ".format(INIT_CFG_FILE)

if file_format == 'config_db':
config_gen_opts += ' -j {} '.format(file)
else:
Expand Down Expand Up @@ -6239,7 +6241,7 @@ def del_subinterface(ctx, subinterface_name):
sub_intfs = [k for k,v in subintf_config_db.items() if type(k) != tuple]
if subinterface_name not in sub_intfs:
ctx.fail("{} does not exists".format(subinterface_name))

ips = {}
ips = [ k[1] for k in config_db.get_table('VLAN_SUB_INTERFACE') if type(k) == tuple and k[0] == subinterface_name ]
for ip in ips:
Expand Down
39 changes: 39 additions & 0 deletions counterpoll/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import click
import json
from flow_counter_util.route import exit_if_route_flow_counter_not_support
from swsscommon.swsscommon import ConfigDBConnector
from tabulate import tabulate

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

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

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

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

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

@cli.command()
def show():
""" Show the counter configuration """
Expand All @@ -363,6 +398,7 @@ def show():
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')
route_info = configdb.get_entry('FLEX_COUNTER_TABLE', 'FLOW_CNT_ROUTE')

header = ("Type", "Interval (in ms)", "Status")
data = []
Expand All @@ -388,6 +424,9 @@ def show():
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)])
if route_info:
data.append(["FLOW_CNT_ROUTE_STAT", route_info.get("POLL_INTERVAL", DEFLT_10_SEC),
route_info.get("FLEX_COUNTER_STATUS", DISABLE)])

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

Expand Down
Empty file added flow_counter_util/__init__.py
Empty file.
Loading

0 comments on commit 3732ac5

Please sign in to comment.