Skip to content

Commit 64604db

Browse files
authored
[acl] Expand VLAN into VLAN members when creating an ACL table (sonic-net#1475)
Signed-off-by: Danny Allen <[email protected]>
1 parent 10de91d commit 64604db

File tree

5 files changed

+190
-13
lines changed

5 files changed

+190
-13
lines changed

config/main.py

+61-13
Original file line numberDiff line numberDiff line change
@@ -3291,37 +3291,85 @@ def get_acl_bound_ports():
32913291

32923292
return list(ports)
32933293

3294-
#
3295-
# 'table' subcommand ('config acl add table ...')
3296-
#
32973294

3298-
@add.command()
3299-
@click.argument("table_name", metavar="<table_name>")
3300-
@click.argument("table_type", metavar="<table_type>")
3301-
@click.option("-d", "--description")
3302-
@click.option("-p", "--ports")
3303-
@click.option("-s", "--stage", type=click.Choice(["ingress", "egress"]), default="ingress")
3304-
def table(table_name, table_type, description, ports, stage):
3295+
def expand_vlan_ports(port_name):
33053296
"""
3306-
Add ACL table
3297+
Expands a given VLAN interface into its member ports.
3298+
3299+
If the provided interface is a VLAN, then this method will return its member ports.
3300+
3301+
If the provided interface is not a VLAN, then this method will return a list with only
3302+
the provided interface in it.
33073303
"""
33083304
config_db = ConfigDBConnector()
33093305
config_db.connect()
33103306

3307+
if port_name not in config_db.get_keys("VLAN"):
3308+
return [port_name]
3309+
3310+
vlan_members = config_db.get_keys("VLAN_MEMBER")
3311+
3312+
members = [member for vlan, member in vlan_members if port_name == vlan]
3313+
3314+
if not members:
3315+
raise ValueError("Cannot bind empty VLAN {}".format(port_name))
3316+
3317+
return members
3318+
3319+
3320+
def parse_acl_table_info(table_name, table_type, description, ports, stage):
33113321
table_info = {"type": table_type}
33123322

33133323
if description:
33143324
table_info["policy_desc"] = description
33153325
else:
33163326
table_info["policy_desc"] = table_name
33173327

3328+
if not ports and ports != None:
3329+
raise ValueError("Cannot bind empty list of ports")
3330+
3331+
port_list = []
3332+
valid_acl_ports = get_acl_bound_ports()
33183333
if ports:
3319-
table_info["ports@"] = ports
3334+
for port in ports.split(","):
3335+
port_list += expand_vlan_ports(port)
3336+
port_list = set(port_list)
33203337
else:
3321-
table_info["ports@"] = ",".join(get_acl_bound_ports())
3338+
port_list = valid_acl_ports
3339+
3340+
for port in port_list:
3341+
if port not in valid_acl_ports:
3342+
raise ValueError("Cannot bind ACL to specified port {}".format(port))
3343+
3344+
table_info["ports@"] = ",".join(port_list)
33223345

33233346
table_info["stage"] = stage
33243347

3348+
return table_info
3349+
3350+
#
3351+
# 'table' subcommand ('config acl add table ...')
3352+
#
3353+
3354+
@add.command()
3355+
@click.argument("table_name", metavar="<table_name>")
3356+
@click.argument("table_type", metavar="<table_type>")
3357+
@click.option("-d", "--description")
3358+
@click.option("-p", "--ports")
3359+
@click.option("-s", "--stage", type=click.Choice(["ingress", "egress"]), default="ingress")
3360+
@click.pass_context
3361+
def table(ctx, table_name, table_type, description, ports, stage):
3362+
"""
3363+
Add ACL table
3364+
"""
3365+
config_db = ConfigDBConnector()
3366+
config_db.connect()
3367+
3368+
try:
3369+
table_info = parse_acl_table_info(table_name, table_type, description, ports, stage)
3370+
except ValueError as e:
3371+
ctx.fail("Failed to parse ACL table config: exception={}".format(e))
3372+
33253373
config_db.set_entry("ACL_TABLE", table_name, table_info)
33263374

33273375
#

doc/Command-Reference.md

+29
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,35 @@ When the optional argument "max_priority" is specified, each rule’s priority
13711371
13721372
Go Back To [Beginning of the document](#) or [Beginning of this section](#acl)
13731373
1374+
**config acl add table**
1375+
1376+
This command is used to create new ACL tables.
1377+
1378+
- Usage:
1379+
```
1380+
config acl add table [OPTIONS] <table_name> <table_type> [-d <description>] [-p <ports>] [-s (ingress | egress)]
1381+
```
1382+
1383+
- Parameters:
1384+
- table_name: The name of the ACL table to create.
1385+
- table_type: The type of ACL table to create (e.g. "L3", "L3V6", "MIRROR")
1386+
- description: A description of the table for the user. (default is the table_name)
1387+
- ports: A comma-separated list of ports/interfaces to add to the table. The behavior is as follows:
1388+
- Physical ports will be bound as physical ports
1389+
- Portchannels will be bound as portchannels - passing a portchannel member is invalid
1390+
- VLANs will be expanded into their members (e.g. "Vlan1000" will become "Ethernet0,Ethernet2,Ethernet4...")
1391+
- stage: The stage this ACL table will be applied to, either ingress or egress. (default is ingress)
1392+
1393+
- Examples:
1394+
```
1395+
admin@sonic:~$ sudo config acl add table EXAMPLE L3 -p Ethernet0,Ethernet4 -s ingress
1396+
```
1397+
```
1398+
admin@sonic:~$ sudo config acl add table EXAMPLE_2 L3V6 -p Vlan1000,PortChannel0001,Ethernet128 -s egress
1399+
```
1400+
1401+
Go Back To [Beginning of the document](#) or [Beginning of this section](#acl)
1402+
13741403
13751404
## ARP & NDP
13761405

tests/acl_config_test.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import pytest
2+
3+
import config.main as config
4+
5+
from click.testing import CliRunner
6+
from config.main import expand_vlan_ports, parse_acl_table_info
7+
8+
9+
class TestConfigAcl(object):
10+
def test_expand_vlan(self):
11+
assert set(expand_vlan_ports("Vlan1000")) == {"Ethernet4", "Ethernet8", "Ethernet12", "Ethernet16"}
12+
13+
def test_expand_lag(self):
14+
assert set(expand_vlan_ports("PortChannel1001")) == {"PortChannel1001"}
15+
16+
def test_expand_physical_interface(self):
17+
assert set(expand_vlan_ports("Ethernet4")) == {"Ethernet4"}
18+
19+
def test_expand_empty_vlan(self):
20+
with pytest.raises(ValueError):
21+
expand_vlan_ports("Vlan3000")
22+
23+
def test_parse_table_with_vlan_expansion(self):
24+
table_info = parse_acl_table_info("TEST", "L3", None, "Vlan1000", "egress")
25+
assert table_info["type"] == "L3"
26+
assert table_info["policy_desc"] == "TEST"
27+
assert table_info["stage"] == "egress"
28+
29+
port_list = table_info["ports@"].split(",")
30+
assert set(port_list) == {"Ethernet4", "Ethernet8", "Ethernet12", "Ethernet16"}
31+
32+
def test_parse_table_with_vlan_and_duplicates(self):
33+
table_info = parse_acl_table_info("TEST", "L3", None, "Ethernet4,Vlan1000", "egress")
34+
assert table_info["type"] == "L3"
35+
assert table_info["policy_desc"] == "TEST"
36+
assert table_info["stage"] == "egress"
37+
38+
# Since Ethernet4 is a member of Vlan1000 we should not include it twice in the output
39+
port_list = table_info["ports@"].split(",")
40+
assert len(port_list) == 4
41+
assert set(port_list) == {"Ethernet4", "Ethernet8", "Ethernet12", "Ethernet16"}
42+
43+
def test_parse_table_with_empty_vlan(self):
44+
with pytest.raises(ValueError):
45+
parse_acl_table_info("TEST", "L3", None, "Ethernet4,Vlan3000", "egress")
46+
47+
def test_parse_table_with_invalid_ports(self):
48+
with pytest.raises(ValueError):
49+
parse_acl_table_info("TEST", "L3", None, "Ethernet200", "egress")
50+
51+
def test_parse_table_with_empty_ports(self):
52+
with pytest.raises(ValueError):
53+
parse_acl_table_info("TEST", "L3", None, "", "egress")
54+
55+
def test_acl_add_table_nonexistent_port(self):
56+
runner = CliRunner()
57+
58+
result = runner.invoke(
59+
config.config.commands["acl"].commands["add"].commands["table"],
60+
["TEST", "L3", "-p", "Ethernet200"])
61+
62+
assert result.exit_code != 0
63+
assert "Cannot bind ACL to specified port Ethernet200" in result.output
64+
65+
def test_acl_add_table_empty_string_port_list(self):
66+
runner = CliRunner()
67+
68+
result = runner.invoke(
69+
config.config.commands["acl"].commands["add"].commands["table"],
70+
["TEST", "L3", "-p", ""])
71+
72+
assert result.exit_code != 0
73+
assert "Cannot bind empty list of ports" in result.output
74+
75+
def test_acl_add_table_empty_vlan(self):
76+
runner = CliRunner()
77+
78+
result = runner.invoke(
79+
config.config.commands["acl"].commands["add"].commands["table"],
80+
["TEST", "L3", "-p", "Vlan3000"])
81+
82+
assert result.exit_code != 0
83+
assert "Cannot bind empty VLAN Vlan3000" in result.output

tests/mock_tables/config_db.json

+3
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,9 @@
434434
"dhcp_servers@": "192.0.0.1,192.0.0.2,192.0.0.3,192.0.0.4",
435435
"vlanid": "2000"
436436
},
437+
"VLAN|Vlan3000": {
438+
"vlanid": "3000"
439+
},
437440
"VLAN_INTERFACE|Vlan1000": {
438441
"NULL": "NULL"
439442
},

tests/vlan_test.py

+14
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
| | | | | 192.0.0.3 | |
2323
| | | | | 192.0.0.4 | |
2424
+-----------+-----------------+------------+----------------+-----------------------+-------------+
25+
| 3000 | | | | | disabled |
26+
+-----------+-----------------+------------+----------------+-----------------------+-------------+
2527
"""
2628

2729
show_vlan_brief_in_alias_mode_output="""\
@@ -38,6 +40,8 @@
3840
| | | | | 192.0.0.3 | |
3941
| | | | | 192.0.0.4 | |
4042
+-----------+-----------------+---------+----------------+-----------------------+-------------+
43+
| 3000 | | | | | disabled |
44+
+-----------+-----------------+---------+----------------+-----------------------+-------------+
4145
"""
4246

4347
show_vlan_brief_empty_output="""\
@@ -49,6 +53,8 @@
4953
| | | | | 192.0.0.3 | |
5054
| | | | | 192.0.0.4 | |
5155
+-----------+-----------------+------------+----------------+-----------------------+-------------+
56+
| 3000 | | | | | disabled |
57+
+-----------+-----------------+------------+----------------+-----------------------+-------------+
5258
"""
5359

5460
show_vlan_brief_with_portchannel_output="""\
@@ -66,6 +72,8 @@
6672
| | | | | 192.0.0.3 | |
6773
| | | | | 192.0.0.4 | |
6874
+-----------+-----------------+-----------------+----------------+-----------------------+-------------+
75+
| 3000 | | | | | disabled |
76+
+-----------+-----------------+-----------------+----------------+-----------------------+-------------+
6977
"""
7078

7179
show_vlan_config_output="""\
@@ -115,6 +123,8 @@
115123
| | | | | 192.0.0.3 | |
116124
| | | | | 192.0.0.4 | |
117125
+-----------+-----------------+------------+----------------+-----------------------+-------------+
126+
| 3000 | | | | | disabled |
127+
+-----------+-----------------+------------+----------------+-----------------------+-------------+
118128
"""
119129

120130
config_add_del_vlan_and_vlan_member_output="""\
@@ -133,6 +143,8 @@
133143
| | | | | 192.0.0.3 | |
134144
| | | | | 192.0.0.4 | |
135145
+-----------+-----------------+------------+----------------+-----------------------+-------------+
146+
| 3000 | | | | | disabled |
147+
+-----------+-----------------+------------+----------------+-----------------------+-------------+
136148
"""
137149

138150
config_add_del_vlan_and_vlan_member_in_alias_mode_output="""\
@@ -151,6 +163,8 @@
151163
| | | | | 192.0.0.3 | |
152164
| | | | | 192.0.0.4 | |
153165
+-----------+-----------------+---------+----------------+-----------------------+-------------+
166+
| 3000 | | | | | disabled |
167+
+-----------+-----------------+---------+----------------+-----------------------+-------------+
154168
"""
155169
class TestVlan(object):
156170
@classmethod

0 commit comments

Comments
 (0)