Skip to content

Commit 31e1c2e

Browse files
committed
[utility] Filter FDB entries
FDB table can get large due to VM creation/deletion which cause fast reboot to slow down. This utility fitlers FDB entries based on current MAC entries in the ARP table. signed-off-by: Tamer Ahmed <tamer.ahmed@microsoft.com> :
1 parent a7b310e commit 31e1c2e

File tree

6 files changed

+4691
-1
lines changed

6 files changed

+4691
-1
lines changed

scripts/filter_fdb_entries.py

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python
2+
3+
import json
4+
import sys
5+
import os
6+
import argparse
7+
import syslog
8+
import traceback
9+
import time
10+
11+
from collections import defaultdict
12+
13+
def get_arp_entries_map(filename):
14+
"""
15+
Generate map for ARP entries
16+
17+
ARP entry map is using the MAC as a key for the arp entry. The map key is reformated in order
18+
to match FDB table formatting
19+
20+
Args:
21+
filename(str): ARP entry file name
22+
23+
Returns:
24+
arp_map(dict) map of ARP entries using MAC as key.
25+
"""
26+
with open(filename, 'r') as fp:
27+
arp_entries = json.load(fp)
28+
29+
arp_map = defaultdict()
30+
for arp in arp_entries:
31+
for key, config in arp.items():
32+
if 'NEIGH_TABLE' in key:
33+
arp_map[config["neigh"].replace(':', '-')] = ""
34+
35+
return arp_map
36+
37+
def filter_fdb_entries(fdb_filename, arp_filename, backup_file):
38+
"""
39+
Filter FDB entries based on MAC presence into ARP entries
40+
41+
FDB entries that do not have MAC entry in the ARP table are filtered out. New FDB entries
42+
file will be created if it has fewer entries than original one.
43+
44+
Args:
45+
fdb_filename(str): FDB entries file name
46+
arp_filename(str): ARP entry file name
47+
backup_file(bool): Create backup copy of FDB file before creating new one
48+
49+
Returns:
50+
None
51+
"""
52+
arp_map = get_arp_entries_map(arp_filename)
53+
54+
with open(fdb_filename, 'r') as fp:
55+
fdb_entries = json.load(fp)
56+
57+
def filter_fdb_entry(fdb_entry):
58+
for key, _ in fdb_entry.items():
59+
if 'FDB_TABLE' in key:
60+
return key.split(':')[-1] in arp_map
61+
62+
new_fdb_entries = list(filter(filter_fdb_entry, fdb_entries))
63+
64+
if len(new_fdb_entries) < len(fdb_entries):
65+
if backup_file:
66+
os.rename(fdb_filename, fdb_filename + '-' + time.strftime("%Y%m%d-%H%M%S"))
67+
68+
with open(fdb_filename, 'w') as fp:
69+
json.dump(new_fdb_entries, fp, indent=2, separators=(',', ': '))
70+
71+
def file_exits_or_raise(filename):
72+
"""
73+
Check if file exist on the file system
74+
75+
Args:
76+
filename(str): File name
77+
78+
Returns:
79+
None
80+
81+
Raises:
82+
Exception file does not exist
83+
"""
84+
if not os.path.exists(filename):
85+
raise Exception("file '{0}' does not exist".format(filename))
86+
87+
def main():
88+
parser = argparse.ArgumentParser()
89+
parser.add_argument('-f', '--fdb', type=str, default='/tmp/fdb.json', help='fdb file name')
90+
parser.add_argument('-a', '--arp', type=str, default='/tmp/arp.json', help='arp file name')
91+
parser.add_argument('-b', '--backup_file', type=bool, default=True, help='Back up old fdb entries file')
92+
args = parser.parse_args()
93+
94+
fdb_filename = args.fdb
95+
file_exits_or_raise(fdb_filename)
96+
97+
arp_filename = args.arp
98+
file_exits_or_raise(arp_filename)
99+
100+
backup_file = args.backup_file
101+
filter_fdb_entries(fdb_filename, arp_filename, backup_file)
102+
103+
return 0
104+
105+
if __name__ == '__main__':
106+
res = 0
107+
try:
108+
syslog.openlog('filter_fdb_entries')
109+
res = main()
110+
except KeyboardInterrupt:
111+
syslog.syslog(syslog.LOG_NOTICE, "SIGINT received. Quitting")
112+
res = 1
113+
except Exception as e:
114+
syslog.syslog(syslog.LOG_ERR, "Got an exception %s: Traceback: %s" % (str(e), traceback.format_exc()))
115+
res = 2
116+
finally:
117+
syslog.closelog()
118+
try:
119+
sys.exit(res)
120+
except SystemExit:
121+
os._exit(res)

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
],
5555
package_data={
5656
'show': ['aliases.ini'],
57-
'sonic-utilities-tests': ['acl_input/*', 'mock_tables/*.py', 'mock_tables/*.json']
57+
'sonic-utilities-tests': ['acl_input/*', 'mock_tables/*.py', 'mock_tables/*.json', 'filter_fdb_input/*.json']
5858
},
5959
scripts=[
6060
'scripts/aclshow',
@@ -74,6 +74,7 @@
7474
'scripts/fast-reboot-dump.py',
7575
'scripts/fdbclear',
7676
'scripts/fdbshow',
77+
'scripts/filter_fdb_entries.py',
7778
'scripts/generate_dump',
7879
'scripts/intfutil',
7980
'scripts/intfstat',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import glob
2+
import json
3+
import os
4+
import pytest
5+
import shutil
6+
import subprocess
7+
import sys
8+
9+
from collections import defaultdict
10+
11+
"""
12+
Filter FDB entries test vector
13+
"""
14+
filterFdbEntriesTestVector = [
15+
{
16+
"arp":[
17+
],
18+
"fdb": [
19+
],
20+
"expected_fdb": [
21+
],
22+
},
23+
{
24+
"arp":[
25+
{
26+
"NEIGH_TABLE:Vlan1000:192.168.0.10": {
27+
"neigh": "72:06:00:01:00:08",
28+
"family": "IPv4"
29+
},
30+
"OP": "SET"
31+
},
32+
],
33+
"fdb": [
34+
{
35+
"FDB_TABLE:Vlan1000:72-06-00-01-01-16": {
36+
"type": "dynamic",
37+
"port": "Ethernet22"
38+
},
39+
"OP": "SET"
40+
},
41+
],
42+
"expected_fdb": [
43+
],
44+
},
45+
{
46+
"arp":[
47+
{
48+
"NEIGH_TABLE:Vlan1000:192.168.0.10": {
49+
"neigh": "72:06:00:01:01:16",
50+
"family": "IPv4"
51+
},
52+
"OP": "SET"
53+
},
54+
],
55+
"fdb": [
56+
{
57+
"FDB_TABLE:Vlan1000:72-06-00-01-01-16": {
58+
"type": "dynamic",
59+
"port": "Ethernet22"
60+
},
61+
"OP": "SET"
62+
},
63+
],
64+
"expected_fdb": [
65+
{
66+
"FDB_TABLE:Vlan1000:72-06-00-01-01-16": {
67+
"type": "dynamic",
68+
"port": "Ethernet22"
69+
},
70+
"OP": "SET"
71+
},
72+
],
73+
},
74+
{
75+
"arp": "sonic-utilities-tests/filter_fdb_input/arp.json",
76+
"fdb": "sonic-utilities-tests/filter_fdb_input/fdb.json",
77+
"expected_fdb": "sonic-utilities-tests/filter_fdb_input/expected_fdb.json"
78+
},
79+
]
80+
81+
class TestFilterFdbEntries(object):
82+
"""
83+
Test Filter FDb entries
84+
"""
85+
ARP_FILENAME = "/tmp/arp.json"
86+
FDB_FILENAME = "/tmp/fdb.json"
87+
EXPECTED_FDB_FILENAME = "/tmp/expected_fdb.json"
88+
89+
def __setUp(self, testData):
90+
"""
91+
Sets up test data
92+
93+
Builds arp.json and fdb.json input files to /tmp and also build expected fdb entries files int /tmp
94+
95+
Args:
96+
testData(dist): Current test vector data
97+
98+
Returns:
99+
None
100+
"""
101+
def create_file_or_raise(data, filename):
102+
"""
103+
Create test data files
104+
105+
If the data is string, it will be dump to a json filename.
106+
If data is a file, it will be coppied to filename
107+
108+
Args:
109+
data(str|list): source of test data
110+
filename(str): filename for test data
111+
112+
Returns:
113+
None
114+
115+
Raises:
116+
Exception if data type is not supported
117+
"""
118+
if isinstance(data, list):
119+
with open(filename, 'w') as fp:
120+
json.dump(data, fp, indent=2, separators=(',', ': '))
121+
elif isinstance(data, str):
122+
shutil.copyfile(data, filename)
123+
else:
124+
raise Exception("Unknown test data type: {0}".format(type(test_data)))
125+
126+
create_file_or_raise(testData["arp"], self.ARP_FILENAME)
127+
create_file_or_raise(testData["fdb"], self.FDB_FILENAME)
128+
create_file_or_raise(testData["expected_fdb"], self.EXPECTED_FDB_FILENAME)
129+
130+
def __tearDown(self):
131+
"""
132+
Tear down current test case setup
133+
134+
Args:
135+
None
136+
137+
Returns:
138+
None
139+
"""
140+
os.remove(self.ARP_FILENAME)
141+
os.remove(self.EXPECTED_FDB_FILENAME)
142+
fdbFiles = glob.glob(self.FDB_FILENAME + '*')
143+
for file in fdbFiles:
144+
os.remove(file)
145+
146+
def __runCommand(self, cmds):
147+
"""
148+
Runs command 'cmds' on host
149+
150+
Args:
151+
cmds(list): command to be run on localhost
152+
153+
Returns:
154+
stdout(str): stdout gathered during command execution
155+
stderr(str): stderr gathered during command execution
156+
returncode(int): command exit code
157+
"""
158+
process = subprocess.Popen(
159+
cmds,
160+
shell=False,
161+
stdout=subprocess.PIPE,
162+
stderr=subprocess.PIPE
163+
)
164+
stdout, stderr = process.communicate()
165+
166+
return stdout, stderr, process.returncode
167+
168+
def __getFdbEntriesMap(self, filename):
169+
"""
170+
Generate map for FDB entries
171+
172+
FDB entry map is using the FDB_TABLE:... as a key for the FDB entry.
173+
174+
Args:
175+
filename(str): FDB entry file name
176+
177+
Returns:
178+
fdbMap(defaultdict) map of FDB entries using MAC as key.
179+
"""
180+
with open(filename, 'r') as fp:
181+
fdbEntries = json.load(fp)
182+
183+
fdbMap = defaultdict()
184+
for fdb in fdbEntries:
185+
for key, config in fdb.items():
186+
if "FDB_TABLE" in key:
187+
fdbMap[key] = fdb
188+
189+
return fdbMap
190+
191+
def __verifyOutput(self):
192+
"""
193+
Verifies FDB entries match expected FDB entries
194+
195+
Args:
196+
None
197+
198+
Retruns:
199+
isEqual(bool): True if FDB entries match, False otherwise
200+
"""
201+
fdbMap = self.__getFdbEntriesMap(self.FDB_FILENAME)
202+
with open(self.EXPECTED_FDB_FILENAME, 'r') as fp:
203+
expectedFdbEntries = json.load(fp)
204+
205+
isEqual = len(fdbMap) == len(expectedFdbEntries)
206+
if isEqual:
207+
for expectedFdbEntry in expectedFdbEntries:
208+
fdbEntry = {}
209+
for key, config in expectedFdbEntry.items():
210+
if "FDB_TABLE" in key:
211+
fdbEntry = fdbMap[key]
212+
213+
isEqual = len(fdbEntry) == len(expectedFdbEntry)
214+
for key, config in expectedFdbEntry.items():
215+
isEqual = isEqual and fdbEntry[key] == config
216+
217+
if not isEqual:
218+
break
219+
220+
return isEqual
221+
222+
@pytest.mark.parametrize("testData", filterFdbEntriesTestVector)
223+
def testFilterFdbEntries(self, testData):
224+
"""
225+
Test Filter FDB entries script
226+
227+
Args:
228+
testData(dict): Map containing ARP entries, FDB entries, and expected FDB entries
229+
"""
230+
try:
231+
self.__setUp(testData)
232+
233+
stdout, stderr, rc = self.__runCommand([
234+
"scripts/filter_fdb_entries.py",
235+
"-a",
236+
self.ARP_FILENAME,
237+
"-f",
238+
self.FDB_FILENAME,
239+
])
240+
assert rc == 0, "CFilter_fbd_entries.py failed with '{0}'".format(stderr)
241+
assert self.__verifyOutput(), "Test failed for test data: {0}".format(testData)
242+
finally:
243+
self.__tearDown()

0 commit comments

Comments
 (0)