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

[utility] Filter FDB entries #890

Merged
merged 7 commits into from
Apr 28, 2020
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
15 changes: 13 additions & 2 deletions scripts/fast-reboot
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ EXIT_NEXT_IMAGE_NOT_EXISTS=4
EXIT_ORCHAGENT_SHUTDOWN=10
EXIT_SYNCD_SHUTDOWN=11
EXIT_FAST_REBOOT_DUMP_FAILURE=12
EXIT_FILTER_FDB_ENTRIES_FAILURE=13
EXIT_NO_CONTROL_PLANE_ASSISTANT=20

function error()
Expand Down Expand Up @@ -370,14 +371,24 @@ fi
if [[ "$REBOOT_TYPE" = "fast-reboot" ]]; then
# Dump the ARP and FDB tables to files also as default routes for both IPv4 and IPv6
# into /host/fast-reboot
mkdir -p /host/fast-reboot
DUMP_DIR=/host/fast-reboot
mkdir -p $DUMP_DIR
FAST_REBOOT_DUMP_RC=0
/usr/bin/fast-reboot-dump.py -t /host/fast-reboot || FAST_REBOOT_DUMP_RC=$?
/usr/bin/fast-reboot-dump.py -t $DUMP_DIR || FAST_REBOOT_DUMP_RC=$?
if [[ FAST_REBOOT_DUMP_RC -ne 0 ]]; then
error "Failed to run fast-reboot-dump.py. Exit code: $FAST_REBOOT_DUMP_RC"
unload_kernel
exit "${EXIT_FAST_REBOOT_DUMP_FAILURE}"
fi

FILTER_FDB_ENTRIES_RC=0
# Filter FDB entries using MAC addresses from ARP table
/usr/bin/filter_fdb_entries.py -f $DUMP_DIR/fdb.json -a $DUMP_DIR/arp.json || FILTER_FDB_ENTRIES_RC=$?
if [[ FILTER_FDB_ENTRIES_RC -ne 0 ]]; then
error "Failed to filter FDb entries. Exit code: $FILTER_FDB_ENTRIES_RC"
unload_kernel
exit "${EXIT_FILTER_FDB_ENTRIES_FAILURE}"
fi
fi

init_warm_reboot_states
Expand Down
127 changes: 127 additions & 0 deletions scripts/filter_fdb_entries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env python

import json
import sys
import os
import argparse
import syslog
import traceback
import time

from collections import defaultdict

def get_arp_entries_map(filename):
"""
Generate map for ARP entries

ARP entry map is using the MAC as a key for the arp entry. The map key is reformated in order
to match FDB table formatting

Args:
filename(str): ARP entry file name

Returns:
arp_map(dict) map of ARP entries using MAC as key.
"""
with open(filename, 'r') as fp:
arp_entries = json.load(fp)

arp_map = defaultdict()
for arp in arp_entries:
for key, config in arp.items():
if 'NEIGH_TABLE' in key:
arp_map[config["neigh"].replace(':', '-')] = ""

return arp_map

def filter_fdb_entries(fdb_filename, arp_filename, backup_file):
"""
Filter FDB entries based on MAC presence into ARP entries

FDB entries that do not have MAC entry in the ARP table are filtered out. New FDB entries
file will be created if it has fewer entries than original one.

Args:
fdb_filename(str): FDB entries file name
arp_filename(str): ARP entry file name
backup_file(bool): Create backup copy of FDB file before creating new one

Returns:
None
"""
arp_map = get_arp_entries_map(arp_filename)

with open(fdb_filename, 'r') as fp:
fdb_entries = json.load(fp)

def filter_fdb_entry(fdb_entry):
for key, _ in fdb_entry.items():
if 'FDB_TABLE' in key:
return key.split(':')[-1] in arp_map

# malformed entry, default to False so it will be deleted
return False

new_fdb_entries = list(filter(filter_fdb_entry, fdb_entries))

if len(new_fdb_entries) < len(fdb_entries):
if backup_file:
os.rename(fdb_filename, fdb_filename + '-' + time.strftime("%Y%m%d-%H%M%S"))

with open(fdb_filename, 'w') as fp:
json.dump(new_fdb_entries, fp, indent=2, separators=(',', ': '))

def file_exists_or_raise(filename):
"""
Check if file exists on the file system

Args:
filename(str): File name

Returns:
None

Raises:
Exception file does not exist
"""
if not os.path.exists(filename):
raise Exception("file '{0}' does not exist".format(filename))

def main():
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--fdb', type=str, default='/tmp/fdb.json', help='fdb file name')
parser.add_argument('-a', '--arp', type=str, default='/tmp/arp.json', help='arp file name')
parser.add_argument('-b', '--backup_file', type=bool, default=True, help='Back up old fdb entries file')
args = parser.parse_args()

fdb_filename = args.fdb
arp_filename = args.arp
backup_file = args.backup_file

try:
file_exists_or_raise(fdb_filename)
file_exists_or_raise(arp_filename)
except Exception as e:
syslog.syslog(syslog.LOG_ERR, "Got an exception %s: Traceback: %s" % (str(e), traceback.format_exc()))
else:
filter_fdb_entries(fdb_filename, arp_filename, backup_file)

return 0

if __name__ == '__main__':
res = 0
try:
syslog.openlog('filter_fdb_entries')
res = main()
except KeyboardInterrupt:
syslog.syslog(syslog.LOG_NOTICE, "SIGINT received. Quitting")
res = 1
except Exception as e:
syslog.syslog(syslog.LOG_ERR, "Got an exception %s: Traceback: %s" % (str(e), traceback.format_exc()))
res = 2
yxieca marked this conversation as resolved.
Show resolved Hide resolved
finally:
syslog.closelog()
try:
sys.exit(res)
except SystemExit:
os._exit(res)
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
],
package_data={
'show': ['aliases.ini'],
'sonic-utilities-tests': ['acl_input/*', 'mock_tables/*.py', 'mock_tables/*.json']
'sonic-utilities-tests': ['acl_input/*', 'mock_tables/*.py', 'mock_tables/*.json', 'filter_fdb_input/*']
},
scripts=[
'scripts/aclshow',
Expand All @@ -74,6 +74,7 @@
'scripts/fast-reboot-dump.py',
'scripts/fdbclear',
'scripts/fdbshow',
'scripts/filter_fdb_entries.py',
'scripts/generate_dump',
'scripts/intfutil',
'scripts/intfstat',
Expand Down
173 changes: 173 additions & 0 deletions sonic-utilities-tests/filter_fdb_entries_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import glob
import json
import os
import pytest
import shutil
import subprocess

from collections import defaultdict
from filter_fdb_input.test_vectors import filterFdbEntriesTestVector

class TestFilterFdbEntries(object):
"""
Test Filter FDb entries
"""
ARP_FILENAME = "/tmp/arp.json"
FDB_FILENAME = "/tmp/fdb.json"
EXPECTED_FDB_FILENAME = "/tmp/expected_fdb.json"

def __setUp(self, testData):
"""
Sets up test data

Builds arp.json and fdb.json input files to /tmp and also build expected fdb entries files int /tmp

Args:
testData(dist): Current test vector data

Returns:
None
"""
def create_file_or_raise(data, filename):
"""
Create test data files

If the data is string, it will be dump to a json filename.
If data is a file, it will be coppied to filename

Args:
data(str|list): source of test data
filename(str): filename for test data

Returns:
None

Raises:
Exception if data type is not supported
"""
if isinstance(data, list):
with open(filename, 'w') as fp:
json.dump(data, fp, indent=2, separators=(',', ': '))
elif isinstance(data, str):
shutil.copyfile(data, filename)
else:
raise Exception("Unknown test data type: {0}".format(type(test_data)))

create_file_or_raise(testData["arp"], self.ARP_FILENAME)
create_file_or_raise(testData["fdb"], self.FDB_FILENAME)
create_file_or_raise(testData["expected_fdb"], self.EXPECTED_FDB_FILENAME)

def __tearDown(self):
"""
Tear down current test case setup

Args:
None

Returns:
None
"""
os.remove(self.ARP_FILENAME)
os.remove(self.EXPECTED_FDB_FILENAME)
fdbFiles = glob.glob(self.FDB_FILENAME + '*')
for file in fdbFiles:
os.remove(file)

def __runCommand(self, cmds):
"""
Runs command 'cmds' on host

Args:
cmds(list): command to be run on localhost

Returns:
stdout(str): stdout gathered during command execution
stderr(str): stderr gathered during command execution
returncode(int): command exit code
"""
process = subprocess.Popen(
cmds,
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = process.communicate()

return stdout, stderr, process.returncode

def __getFdbEntriesMap(self, filename):
"""
Generate map for FDB entries

FDB entry map is using the FDB_TABLE:... as a key for the FDB entry.

Args:
filename(str): FDB entry file name

Returns:
fdbMap(defaultdict) map of FDB entries using MAC as key.
"""
with open(filename, 'r') as fp:
fdbEntries = json.load(fp)

fdbMap = defaultdict()
for fdb in fdbEntries:
for key, config in fdb.items():
if "FDB_TABLE" in key:
fdbMap[key] = fdb

return fdbMap

def __verifyOutput(self):
"""
Verifies FDB entries match expected FDB entries

Args:
None

Retruns:
isEqual(bool): True if FDB entries match, False otherwise
"""
fdbMap = self.__getFdbEntriesMap(self.FDB_FILENAME)
with open(self.EXPECTED_FDB_FILENAME, 'r') as fp:
expectedFdbEntries = json.load(fp)

isEqual = len(fdbMap) == len(expectedFdbEntries)
if isEqual:
for expectedFdbEntry in expectedFdbEntries:
fdbEntry = {}
for key, config in expectedFdbEntry.items():
if "FDB_TABLE" in key:
fdbEntry = fdbMap[key]

isEqual = len(fdbEntry) == len(expectedFdbEntry)
for key, config in expectedFdbEntry.items():
isEqual = isEqual and fdbEntry[key] == config

if not isEqual:
break

return isEqual

@pytest.mark.parametrize("testData", filterFdbEntriesTestVector)
def testFilterFdbEntries(self, testData):
"""
Test Filter FDB entries script

Args:
testData(dict): Map containing ARP entries, FDB entries, and expected FDB entries
"""
try:
self.__setUp(testData)

stdout, stderr, rc = self.__runCommand([
"scripts/filter_fdb_entries.py",
"-a",
self.ARP_FILENAME,
"-f",
self.FDB_FILENAME,
])
assert rc == 0, "CFilter_fbd_entries.py failed with '{0}'".format(stderr)
assert self.__verifyOutput(), "Test failed for test data: {0}".format(testData)
finally:
self.__tearDown()
Empty file.
Loading