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 CLI for route flow counter feature #2031

Merged
merged 9 commits into from
Apr 18, 2022
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
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