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 support for port mirroring CLIs #936

Merged
merged 8 commits into from
Jul 1, 2020
Merged
Show file tree
Hide file tree
Changes from 4 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
29 changes: 19 additions & 10 deletions acl_loader/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,21 +718,30 @@ def show_session(self, session_name):
:param session_name: Optional. Mirror session name. Filter sessions by specified name.
:return:
"""
header = ("Name", "Status", "SRC IP", "DST IP", "GRE", "DSCP", "TTL", "Queue", "Policer", "Monitor Port")
erspan_header = ("Name", "Status", "SRC IP", "DST IP", "GRE", "DSCP", "TTL", "Queue",
"Policer", "Monitor Port", "SRC Port", "Direction")
span_header = ("Name", "Status", "DST Port", "SRC Port", "Direction", "Queue", "Policer")

data = []
erspan_data = []
span_data = []
for key, val in self.get_sessions_db_info().iteritems():
if session_name and key != session_name:
continue
# For multi-mpu platform status and monitor port will be dict()
# of 'asic-x':value
data.append([key, val["status"], val["src_ip"], val["dst_ip"],
val.get("gre_type", ""), val.get("dscp", ""),
val.get("ttl", ""), val.get("queue", ""), val.get("policer", ""),
val.get("monitor_port", "")])

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

if val.get("type") == "SPAN":

Choose a reason for hiding this comment

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

What is the need for policer and queue for SPAN sessions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

same as above. Extending it to SPAN also.

span_data.append([key, val.get("status", ""), val.get("dst_port", ""),
val.get("src_port", ""), val.get("direction", "").lower(),
val.get("queue", ""), val.get("policer", "")])
daall marked this conversation as resolved.
Show resolved Hide resolved
else:
erspan_data.append([key, val.get("status", ""), val.get("src_ip", ""),
val.get("dst_ip", ""), val.get("gre_type", ""), val.get("dscp", ""),
val.get("ttl", ""), val.get("queue", ""), val.get("policer", ""),
val.get("monitor_port", ""), val.get("src_port", ""), val.get("direction", "").lower()])

print("ERSPAN Sessions")
print(tabulate.tabulate(erspan_data, headers=erspan_header, tablefmt="simple", missingval=""))
print("\nSPAN Sessions")
print(tabulate.tabulate(span_data, headers=span_header, tablefmt="simple", missingval=""))

def show_policer(self, policer_name):
"""
Expand Down
233 changes: 219 additions & 14 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,102 @@ def is_ipaddress(val):
return False
return True

def interface_is_in_vlan(vlan_member_table, interface_name):
""" Check if an interface is in a vlan """
for _,v in vlan_member_table.keys():
if v == interface_name:
return True

return False

def interface_is_in_portchannel(portchannel_member_table, interface_name):
""" Check if an interface is part of portchannel """
for _,v in portchannel_member_table.keys():
if v == interface_name:
return True

return False

def interface_is_router_port(interface_table, interface_name):
""" Check if an interface has router config """
for entry in interface_table.keys():
daall marked this conversation as resolved.
Show resolved Hide resolved
if (interface_name == entry[0]):
return True

return False

def interface_is_mirror_dst_port(config_db, interface_name):
""" Check if port is already configured as mirror destination port """
mirror_table = config_db.get_table('MIRROR_SESSION')
for _,v in mirror_table.items():
if 'dst_port' in v and v['dst_port'] == interface_name:
return True

return False

def interface_has_mirror_config(mirror_table, interface_name):
""" Check if port is already configured with mirror config """
for _,v in mirror_table.items():
if 'src_port' in v and v['src_port'] == interface_name:
return True
if 'dst_port' in v and v['dst_port'] == interface_name:
return True

return False

#
# Check if SPAN mirror-session config is valid.
#
daall marked this conversation as resolved.
Show resolved Hide resolved
def validate_mirror_session_config(config_db, session_name, dst_port, src_port, direction):
daall marked this conversation as resolved.
Show resolved Hide resolved
""" Check if SPAN mirror-session config is valid """
if len(config_db.get_entry('MIRROR_SESSION', session_name)) != 0:
click.echo("Error: {} already exists".format(session_name))
return False

vlan_member_table = config_db.get_table('VLAN_MEMBER')
mirror_table = config_db.get_table('MIRROR_SESSION')
portchannel_member_table = config_db.get_table('PORTCHANNEL_MEMBER')
interface_table = config_db.get_table('INTERFACE')

if dst_port is not None:
if interface_name_is_valid(dst_port) is False:
click.echo("Error: Destination Interface {} is invalid".format(dst_port))
return False

if interface_is_in_vlan(vlan_member_table, dst_port):

Choose a reason for hiding this comment

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

If the port is a routed port, SPAN session is supported?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. Not supported. Please check in latest patchset.

click.echo("Error: Destination Interface {} has vlan config".format(dst_port))
return False

if interface_has_mirror_config(mirror_table, dst_port):

Choose a reason for hiding this comment

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

Not sure if I understand this correctly, can a single dst port cannot be a monitor (mirror-to) port
for multiple mirror sessions?
For example
case 1: mirror rx from Ethernet0 to Etherent4
case 2: mirror rx&tx from Ethernet2 to Etherent4
with the above check, case 2 will fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. This is not supported. We allow only one mirror session per port, The session can have any number of ports.

click.echo("Error: Destination Interface {} already has mirror config".format(dst_port))
return False

if interface_is_in_portchannel(portchannel_member_table, dst_port):
click.echo("Error: Destination Interface {} has portchannel config".format(dst_port))
return False

if interface_is_router_port(interface_table, dst_port):
click.echo("Error: Destination Interface {} is a L3 interface".format(dst_port))
return False

if src_port is not None:
daall marked this conversation as resolved.
Show resolved Hide resolved
for port in src_port.split(","):
if interface_name_is_valid(port) is False:
click.echo("Error: Source Interface {} is invalid".format(port))
return False
if dst_port is not None and dst_port == port:
click.echo("Error: Destination Interface cant be same as Source Interface")
return False
if interface_has_mirror_config(mirror_table, port):
click.echo("Error: Source Interface {} already has mirror config".format(port))
return False

if direction is not None:
if not any ( [direction == 'rx', direction == 'tx', direction == 'both'] ):
daall marked this conversation as resolved.
Show resolved Hide resolved
click.echo("Error: Direction {} is invalid".format(direction))
daall marked this conversation as resolved.
Show resolved Hide resolved
return False

return True

# This is our main entrypoint - the main 'config' command
@click.group(cls=AbbreviationGroup, context_settings=CONTEXT_SETTINGS)
Expand Down Expand Up @@ -996,6 +1092,8 @@ def portchannel_member(ctx):
def add_portchannel_member(ctx, portchannel_name, port_name):
"""Add member to port channel"""
db = ctx.obj['db']
if interface_is_mirror_dst_port(db, port_name):
ctx.fail("{} is configured as mirror destination port".format(interface_name))
db.set_entry('PORTCHANNEL_MEMBER', (portchannel_name, port_name),
{'NULL': 'NULL'})

Expand All @@ -1017,7 +1115,11 @@ def del_portchannel_member(ctx, portchannel_name, port_name):
def mirror_session():
pass

@mirror_session.command()
#
# 'add' subgroup ('config mirror_session add ...')
#

@mirror_session.command('add')
@click.argument('session_name', metavar='<session_name>', required=True)
@click.argument('src_ip', metavar='<src_ip>', required=True)
@click.argument('dst_ip', metavar='<dst_ip>', required=True)
Expand All @@ -1027,46 +1129,145 @@ def mirror_session():
@click.argument('queue', metavar='[queue]', required=False)
@click.option('--policer')
def add(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer):
"""
Add mirror session
"""
""" Add ERSPAN mirror session.(Legacy support) """
add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer)

@mirror_session.group(cls=AbbreviationGroup, name='erspan')
@click.pass_context
def erspan(ctx):
""" ERSPAN mirror_session """
pass


#
# 'add' subcommand
#

@erspan.command('add')
@click.argument('session_name', metavar='<session_name>', required=True)
@click.argument('src_ip', metavar='<src_ip>', required=True)
@click.argument('dst_ip', metavar='<dst_ip>', required=True)
@click.argument('dscp', metavar='<dscp>', required=True)
@click.argument('ttl', metavar='<ttl>', required=True)
@click.argument('gre_type', metavar='[gre_type]', required=False)
@click.argument('queue', metavar='[queue]', required=False)
@click.argument('src_port', metavar='[src_port]', required=False)
@click.argument('direction', metavar='[direction]', required=False)
@click.option('--policer')
def add(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer, src_port, direction):
""" Add ERSPAN mirror session """
add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer, src_port, direction)

def mirror_common_init(session_info, policer, queue, src_port, direction):
daall marked this conversation as resolved.
Show resolved Hide resolved
if policer is not None:
session_info['policer'] = policer

if queue is not None:
session_info['queue'] = queue

if src_port is not None:
if get_interface_naming_mode() == "alias":
src_port_list = []
for port in src_port.split(","):
src_port_list.append(interface_alias_to_name(port))
src_port=",".join(src_port_list)

session_info['src_port'] = src_port
if direction is None:
daall marked this conversation as resolved.
Show resolved Hide resolved
direction = "both"

if direction is not None:
daall marked this conversation as resolved.
Show resolved Hide resolved
session_info['direction'] = direction.upper()
return session_info

def add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer, src_port=None, direction=None):
session_info = {
"type" : "ERSPAN",
"src_ip": src_ip,
"dst_ip": dst_ip,
"dscp": dscp,
"ttl": ttl
}

if policer is not None:
session_info['policer'] = policer

if gre_type is not None:
session_info['gre_type'] = gre_type

if queue is not None:
session_info['queue'] = queue

session_info = mirror_common_init(session_info, policer, queue, src_port, direction)

"""
For multi-npu platforms we need to program all front asic namespaces
"""
namespaces = sonic_device_util.get_all_namespaces()
if not namespaces['front_ns']:
config_db = ConfigDBConnector()
config_db.connect()
if validate_mirror_session_config(config_db, session_name, None, src_port, direction) is False:
daall marked this conversation as resolved.
Show resolved Hide resolved
return
config_db.set_entry("MIRROR_SESSION", session_name, session_info)
else:
Copy link
Contributor

Choose a reason for hiding this comment

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

@abdosi could you take a quick look at the multi-NPU logic here and make sure it looks OK to you?

Copy link
Contributor

Choose a reason for hiding this comment

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

Validation need to be updated so that src_port and dst_port are in same asic/namespace.
TO_DO item (not blocking for this PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok. will take of this in next PR. How do I validate this ?

per_npu_configdb = {}
for front_asic_namespaces in namespaces['front_ns']:
per_npu_configdb[front_asic_namespaces] = ConfigDBConnector(use_unix_socket_path=True, namespace=front_asic_namespaces)
per_npu_configdb[front_asic_namespaces].connect()
if validate_mirror_session_config(per_npu_configdb[front_asic_namespaces], session_name, None, src_port, direction) is False:
return
per_npu_configdb[front_asic_namespaces].set_entry("MIRROR_SESSION", session_name, session_info)

@mirror_session.command()
@mirror_session.group(cls=AbbreviationGroup, name='span')
@click.pass_context
def span(ctx):
""" SPAN mirror session """
pass

@span.command('add')
@click.argument('session_name', metavar='<session_name>', required=True)
def remove(session_name):
@click.argument('dst_port', metavar='<dst_port>', required=True)
@click.argument('src_port', metavar='[src_port]', required=False)
@click.argument('direction', metavar='[direction]', required=False)
@click.argument('queue', metavar='[queue]', required=False)
@click.option('--policer')
def add(session_name, dst_port, src_port, direction, queue, policer):
""" Add SPAN mirror session """
add_span(session_name, dst_port, src_port, direction, queue, policer)

def add_span(session_name, dst_port, src_port, direction, queue, policer):
if get_interface_naming_mode() == "alias":
dst_port = interface_alias_to_name(dst_port)
if dst_port is None:
click.echo("Error: Destination Interface {} is invalid".format(dst_port))
return

session_info = {
"type" : "SPAN",
"dst_port": dst_port,
}

session_info = mirror_common_init(session_info, policer, queue, src_port, direction)

"""
Delete mirror session
For multi-npu platforms we need to program all front asic namespaces
"""
namespaces = sonic_device_util.get_all_namespaces()
abdosi marked this conversation as resolved.
Show resolved Hide resolved
if not namespaces['front_ns']:
config_db = ConfigDBConnector()
config_db.connect()
if validate_mirror_session_config(config_db, session_name, dst_port, src_port, direction) is False:
return
config_db.set_entry("MIRROR_SESSION", session_name, session_info)
else:
per_npu_configdb = {}
for front_asic_namespaces in namespaces['front_ns']:
per_npu_configdb[front_asic_namespaces] = ConfigDBConnector(use_unix_socket_path=True, namespace=front_asic_namespaces)
per_npu_configdb[front_asic_namespaces].connect()
if validate_mirror_session_config(per_npu_configdb[front_asic_namespaces], session_name, dst_port, src_port, direction) is False:
Copy link
Contributor

Choose a reason for hiding this comment

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

Validation need to be updated so that src_port and dst_port are in same asic/namespace.
TO_DO item (not blocking for this PR)

return
per_npu_configdb[front_asic_namespaces].set_entry("MIRROR_SESSION", session_name, session_info)


@mirror_session.command()
@click.argument('session_name', metavar='<session_name>', required=True)
def remove(session_name):
""" Delete mirror session """

"""
For multi-npu platforms we need to program all front asic namespaces
Expand All @@ -1082,6 +1283,7 @@ def remove(session_name):
per_npu_configdb[front_asic_namespaces] = ConfigDBConnector(use_unix_socket_path=True, namespace=front_asic_namespaces)
per_npu_configdb[front_asic_namespaces].connect()
per_npu_configdb[front_asic_namespaces].set_entry("MIRROR_SESSION", session_name, None)

#
# 'pfcwd' group ('config pfcwd ...')
#
Expand Down Expand Up @@ -1356,6 +1558,9 @@ def add_vlan_member(ctx, vid, interface_name, untagged):

if len(vlan) == 0:
ctx.fail("{} doesn't exist".format(vlan_name))
if interface_is_mirror_dst_port(db, interface_name):
ctx.fail("{} is configured as mirror destination port".format(interface_name))

members = vlan.get('members', [])
if interface_name in members:
if get_interface_naming_mode() == "alias":
Expand All @@ -1370,7 +1575,7 @@ def add_vlan_member(ctx, vid, interface_name, untagged):
for entry in interface_table:
if (interface_name == entry[0]):
ctx.fail("{} is a L3 interface!".format(interface_name))

members.append(interface_name)
vlan['members'] = members
db.set_entry('VLAN', vlan_name, vlan)
Expand Down
Loading