Skip to content
Open
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
9 changes: 6 additions & 3 deletions acl_loader/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1002,8 +1002,10 @@ def show_session(self, session_name):
:param session_name: Optional. Mirror session name. Filter sessions by specified name.
:return:
"""
erspan_header = ("Name", "Status", "SRC IP", "DST IP", "GRE", "DSCP", "TTL", "Queue",
"Policer", "Monitor Port", "SRC Port", "Direction")
erspan_header = ("Name", "Status", "SRC IP", "DST IP", "GRE", "DSCP",
"TTL", "Queue", "Policer", "Monitor Port",
"SRC Port", "Direction",
"Sample Rate", "Truncate Size")
span_header = ("Name", "Status", "DST Port", "SRC Port", "Direction", "Queue", "Policer")

erspan_data = []
Expand All @@ -1020,7 +1022,8 @@ def show_session(self, session_name):
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()])
val.get("monitor_port", ""), val.get("src_port", ""), val.get("direction", "").lower(), # noqa: E127, E501
val.get("sample_rate", ""), val.get("truncate_size", "")]) # noqa: E127

print("ERSPAN Sessions")
erspan_data = natsorted(erspan_data)
Expand Down
50 changes: 44 additions & 6 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,25 @@

asic_type = None


SAMPLE_RATE_MIN = 256
SAMPLE_RATE_MAX = 8388608
TRUNCATE_SIZE_MIN = 64
TRUNCATE_SIZE_MAX = 9216

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor nit: The error message must be 256..8388608 doesn't mention that 0 is also a valid value (disabled). A user seeing this error might not realize 0 is accepted. Consider: must be 0 (disabled) or 256..8388608. Same for validate_truncate_size. Low priority — not blocking.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still a valid nit. The error message says must be 256..8388608 but doesn't mention 0 is also accepted. Low priority, but would improve UX.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still open. Error messages still say must be 256..8388608 / must be 64..9216 without mentioning 0 is valid. Low priority nit but would improve UX.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still valid nit — error message omits that 0 is accepted. Non-blocking, but would be a nice UX improvement in a follow-up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Addressed by new design direction. Help text now says "Omit for full mirror" instead of "0 = disabled", so the error message must be 256..8388608 is consistent — users are not expected to explicitly pass 0. The magic numbers are now constants too. Looks good.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The error message must be 256..8388608 does not mention that 0 is also a valid value (for disabling). A user seeing this error might not realize 0 is accepted. Consider updating the message to something like: must be 0 (disabled) or 256..8388608.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Still open — same concern as the other thread. Error message doesn't mention 0 is accepted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same nit — error message doesn't mention 0 is valid. Non-blocking.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same as above — the reframed help text ("Omit for full mirror" / "Omit for no truncation") makes the error message consistent. No longer need to mention 0. Resolved.


def validate_sample_rate(ctx, param, value):
Comment thread
Janetxxx marked this conversation as resolved.
if value != 0 and (value < SAMPLE_RATE_MIN or value > SAMPLE_RATE_MAX):
raise click.BadParameter(f"must be {SAMPLE_RATE_MIN}..{SAMPLE_RATE_MAX}")
return value


def validate_truncate_size(ctx, param, value):
if value != 0 and (value < TRUNCATE_SIZE_MIN or value > TRUNCATE_SIZE_MAX):
raise click.BadParameter(f"must be {TRUNCATE_SIZE_MIN}..{TRUNCATE_SIZE_MAX}")
return value


DSCP_RANGE = click.IntRange(min=0, max=63)
TTL_RANGE = click.IntRange(min=0, max=255)
QUEUE_RANGE = click.IntRange(min=0, max=255)
Expand Down Expand Up @@ -3190,12 +3209,20 @@ def erspan(ctx):
@click.argument('src_port', metavar='[src_port]', required=False)
@click.argument('direction', metavar='[direction]', required=False)
@click.option('--policer')
def erspan_add(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer, src_port, direction):
@click.option('--sample_rate', type=int, default=0, callback=validate_sample_rate,
help='Sampling rate (1-in-N). Valid range: 256..8388608. Omit for full mirror.')
@click.option('--truncate_size', type=int, default=0, callback=validate_truncate_size,
help='Truncation size in bytes. Valid range: 64..9216. Omit for no truncation.')
def erspan_add(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer, src_port, direction,
sample_rate, truncate_size):
""" Add ERSPAN mirror session """
add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer, src_port, direction)
add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type,
queue, policer, src_port, direction,
sample_rate, truncate_size)


def gather_session_info(session_info, policer, queue, src_port, direction):
def gather_session_info(session_info, policer, queue, src_port, direction,
sample_rate=0, truncate_size=0):
if policer:
session_info['policer'] = policer

Expand All @@ -3208,9 +3235,18 @@ def gather_session_info(session_info, policer, queue, src_port, direction):
direction = "both"
Comment thread
Janetxxx marked this conversation as resolved.
session_info['direction'] = direction.upper()

if sample_rate:
session_info['sample_rate'] = str(sample_rate)

if truncate_size:
session_info['truncate_size'] = str(truncate_size)

return session_info

def add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer, src_port=None, direction=None):

def add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue,
policer, src_port=None, direction=None,
sample_rate=0, truncate_size=0):
session_info = {
"type" : "ERSPAN",
"src_ip": src_ip,
Expand All @@ -3222,7 +3258,8 @@ def add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer
if gre_type is not None:
session_info['gre_type'] = gre_type

session_info = gather_session_info(session_info, policer, queue, src_port, direction)
session_info = gather_session_info(session_info, policer, queue, src_port, direction,
sample_rate, truncate_size)
raw_src_port = session_info.get('src_port')
ctx = click.get_current_context()

Expand Down Expand Up @@ -3333,6 +3370,7 @@ def span_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):
# Save original dst_port for namespace detection (before alias conversion)
original_dst_port = dst_port
Expand Down Expand Up @@ -3435,7 +3473,7 @@ def remove(session_name):
else:
per_npu_configdb = {}
for front_asic_namespaces in namespaces['front_ns']:
per_npu_configdb[front_asic_namespaces] = ValidatedConfigDBConnector(ConfigDBConnector(use_unix_socket_path=True, namespace=front_asic_namespaces))
per_npu_configdb[front_asic_namespaces] = ValidatedConfigDBConnector(ConfigDBConnector(use_unix_socket_path=True, namespace=front_asic_namespaces)) # noqa: E501
per_npu_configdb[front_asic_namespaces].connect()
if per_npu_configdb[front_asic_namespaces].get_entry("MIRROR_SESSION", session_name):
try:
Expand Down
4 changes: 3 additions & 1 deletion doc/Command-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -9256,10 +9256,12 @@ While adding a new ERSPAN session, users need to configure the following fields
7) optional - Policer which will be used to control the rate at which frames are mirrored.
8) optional - List of source ports which can have both Ethernet and LAG ports.
9) optional - Direction - Mirror session direction when configured along with Source port. (Supported rx/tx/both. default direction is both)
10) optional - Sample rate for sampled mirroring. N means mirror 1-in-N packets. When not specified, full mirroring is used. Valid range: 256..8388608.
11) optional - Truncate size in bytes for mirrored packets. When not specified, no truncation is applied. Valid range: 64..9216.

- Usage:
```
config mirror_session erspan add <session_name> <src_ip> <dst_ip> <dscp> <ttl> [gre_type] [queue] [policer <policer_name>] [source-port-list] [direction]
config mirror_session erspan add <session_name> <src_ip> <dst_ip> <dscp> <ttl> [gre_type] [queue] [policer <policer_name>] [source-port-list] [direction] [--sample_rate <value>] [--truncate_size <value>]
```

The following command is also supported to be backward compatible.
Expand Down
126 changes: 123 additions & 3 deletions tests/config_mirror_session_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,19 +162,19 @@ def test_mirror_session_erspan_add():
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "100.1.1.1", "2.2.2.2", "8", "63", "10", "100"])

mocked.assert_called_with("test_session", "100.1.1.1", "2.2.2.2", 8, 63, 10, 100, None, None, None)
mocked.assert_called_with("test_session", "100.1.1.1", "2.2.2.2", 8, 63, 10, 100, None, None, None, 0, 0)

result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "100.1.1.1", "2.2.2.2", "8", "63", "0x1234", "100"])

mocked.assert_called_with("test_session", "100.1.1.1", "2.2.2.2", 8, 63, 0x1234, 100, None, None, None)
mocked.assert_called_with("test_session", "100.1.1.1", "2.2.2.2", 8, 63, 0x1234, 100, None, None, None, 0, 0)

result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "100.1.1.1", "2.2.2.2", "8", "63", "0", "0"])

mocked.assert_called_with("test_session", "100.1.1.1", "2.2.2.2", 8, 63, 0, 0, None, None, None)
mocked.assert_called_with("test_session", "100.1.1.1", "2.2.2.2", 8, 63, 0, 0, None, None, None, 0, 0)


@patch("validated_config_db_connector.device_info.is_yang_config_validation_enabled", mock.Mock(return_value=True))
Expand Down Expand Up @@ -799,3 +799,123 @@ def test_mirror_session_capability_function():
assert config.is_port_mirror_capability_supported("tx") is True
assert config.is_port_mirror_capability_supported("both") is True
assert config.is_port_mirror_capability_supported(None) is True


def test_mirror_session_erspan_add_with_invalid_sample_rate():
runner = CliRunner()

# Verify invalid sample_rate (negative)
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--sample_rate", "-1"])
assert result.exit_code != 0
assert 'must be 256..8388608' in result.output

# Verify invalid truncate_size (negative)
result = runner.invoke(
Comment thread
Janetxxx marked this conversation as resolved.
Comment thread
Janetxxx marked this conversation as resolved.
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--truncate_size", "-1"])
assert result.exit_code != 0
Comment thread
Janetxxx marked this conversation as resolved.
Comment thread
Janetxxx marked this conversation as resolved.
Comment thread
Janetxxx marked this conversation as resolved.
assert 'must be 64..9216' in result.output


def test_mirror_session_erspan_add_sample_rate_boundary():
runner = CliRunner()

# sample_rate=1 (in the 1-255 hole, should fail)
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--sample_rate", "1"])
assert result.exit_code != 0
assert 'must be 256..8388608' in result.output

# sample_rate=255 (upper boundary of hole, should fail)
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--sample_rate", "255"])
assert result.exit_code != 0
assert 'must be 256..8388608' in result.output

# sample_rate=256 (minimum valid, should pass)
with mock.patch('config.main.add_erspan') as _:
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--sample_rate", "256"])
assert result.exit_code == 0

# sample_rate=8388608 (maximum valid, should pass)
with mock.patch('config.main.add_erspan') as _:
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--sample_rate", "8388608"])
assert result.exit_code == 0

# sample_rate=8388609 (above maximum, should fail)
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--sample_rate", "8388609"])
assert result.exit_code != 0
assert 'must be 256..8388608' in result.output


def test_mirror_session_erspan_add_truncate_size_boundary():
runner = CliRunner()

# truncate_size=1 (in the 1-63 hole, should fail)
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--truncate_size", "1"])
assert result.exit_code != 0
assert 'must be 64..9216' in result.output

# truncate_size=63 (upper boundary of hole, should fail)
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--truncate_size", "63"])
assert result.exit_code != 0
assert 'must be 64..9216' in result.output

# truncate_size=64 (minimum valid, should pass)
with mock.patch('config.main.add_erspan') as _:
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--truncate_size", "64"])
assert result.exit_code == 0

# truncate_size=9216 (maximum valid, should pass)
with mock.patch('config.main.add_erspan') as _:
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--truncate_size", "9216"])
assert result.exit_code == 0

# truncate_size=9217 (above maximum, should fail)
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "1.1.1.1", "2.2.2.2", "8", "64",
"--truncate_size", "9217"])
assert result.exit_code != 0
assert 'must be 64..9216' in result.output


def test_mirror_session_erspan_add_with_valid_sample_rate_and_truncate():
runner = CliRunner()
with mock.patch('config.main.add_erspan') as mocked:
result = runner.invoke(
config.config.commands["mirror_session"].commands["erspan"].commands["add"],
["test_session", "100.1.1.1", "2.2.2.2", "8", "64",
"--sample_rate", "50000", "--truncate_size", "128"])
assert result.exit_code == 0
mocked.assert_called_with("test_session", "100.1.1.1", "2.2.2.2",
8, 64, None, None, None, None, None, 50000, 128)
12 changes: 12 additions & 0 deletions tests/mock_tables/config_db.json
Original file line number Diff line number Diff line change
Expand Up @@ -3254,6 +3254,18 @@
"src_port": "Ethernet40,Ethernet48",
"direction": "RX"
},
"MIRROR_SESSION|test_session_sampled": {
"type": "ERSPAN",
"src_ip": "10.0.0.1",
"dst_ip": "10.0.0.2",
"dscp": "8",
"ttl": "64",
"gre_type": "0x8949",
"src_port": "Ethernet0",
"direction": "RX",
"sample_rate": "50000",
"truncate_size": "128"
},
"FABRIC_MONITOR|FABRIC_MONITOR_DATA": {
"monCapacityThreshWarn": "100",
"monErrThreshCrcCells": "1",
Expand Down
11 changes: 7 additions & 4 deletions tests/show_mirror_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# flake8: noqa: E501
Comment thread
Janetxxx marked this conversation as resolved.
Comment thread
Janetxxx marked this conversation as resolved.
import os
import sys
from swsscommon.swsscommon import SonicV2Connector
Expand All @@ -6,7 +7,7 @@

import acl_loader.main as acl_loader_show
from acl_loader import *
from acl_loader.main import *
from acl_loader.main import * # noqa: F403, F401

class TestShowMirror(object):
def test_mirror_show(self):
Expand All @@ -22,9 +23,10 @@ def test_mirror_show(self):
}
expected_output = """\
ERSPAN Sessions
Name Status SRC IP DST IP GRE DSCP TTL Queue Policer Monitor Port SRC Port Direction
---------------- -------- -------- -------- ----- ------ ----- ------- --------- -------------- --------------------- -----------
test_session_db1 active Ethernet40,Ethernet48 rx
Name Status SRC IP DST IP GRE DSCP TTL Queue Policer Monitor Port SRC Port Direction Sample Rate Truncate Size
-------------------- -------- -------- -------- ------ ------ ----- ------- --------- -------------- --------------------- ----------- ------------- ---------------
test_session_db1 active Ethernet40,Ethernet48 rx
test_session_sampled active 10.0.0.1 10.0.0.2 0x8949 8 64 Ethernet0 rx 50000 128

SPAN Sessions
Name Status DST Port SRC Port Direction Queue Policer
Expand All @@ -41,3 +43,4 @@ def test_mirror_show(self):
result_output = result.output.replace('error', 'active')
result_output = result_output.replace('inactive', 'active')
assert result_output == expected_output

Loading