diff --git a/scripts/bmpcfgd b/scripts/bmpcfgd new file mode 100644 index 00000000..914ce642 --- /dev/null +++ b/scripts/bmpcfgd @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +''' +bmpcfgd +Daemon which monitors bmp relevant table enablement from CONFIG_DB, and reset BMP states +''' + +import os +import sys +import subprocess +import syslog +import signal +from shutil import copy2 +from datetime import datetime +from sonic_py_common import device_info +from sonic_py_common.general import check_output_pipe +from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table +from swsscommon import swsscommon + +CFG_DB = "CONFIG_DB" +STATE_DB = "STATE_DB" + + +class BMPCfg(object): + def __init__(self, state_db_conn): + self.bgp_neighbor_table = False + self.bgp_rib_in_table = False + self.bgp_rib_out_table = False + self.state_db_conn = state_db_conn + + + def load(self, data={}): + common_config = data.get('table', {}) + if not common_config: + syslog.syslog(syslog.LOG_INFO, f'BMPCfg: skipped the bmp config, the BMP setting is empty.') + return + + self.bgp_neighbor_table = is_true(common_config.get('bgp_neighbor_table', 'false')) + self.bgp_rib_in_table = is_true(common_config.get('bgp_rib_in_table', 'false')) + self.bgp_rib_out_table = is_true(common_config.get('bgp_rib_out_table', 'false')) + syslog.syslog(syslog.LOG_INFO, f'BMPCfg: update : {self.bgp_neighbor_table}, {self.bgp_rib_in_table}, {self.bgp_rib_out_table}.') + + # reset bmp table state once config is changed. + self.stop_bmp() + self.reset_bmp_table() + self.start_bmp() + + def cfg_handler(self, data): + self.load(data) + + def stop_bmp(self): + syslog.syslog(syslog.LOG_INFO, 'stop bmp daemon') + subprocess.call(["service", "openbmpd", "stop"]) + + def reset_bmp_table(self): + syslog.syslog(syslog.LOG_INFO, 'Reset bmp table from state_db') + self.state_db_conn.delete_all_by_pattern('STATE_DB', 'BGP_NEIGHBOR*') + self.state_db_conn.delete_all_by_pattern('STATE_DB', 'BGP_RIB_IN_TABLE*') + self.state_db_conn.delete_all_by_pattern('STATE_DB', 'BGP_RIB_OUT_TABLE*') + + def start_bmp(self): + syslog.syslog(syslog.LOG_INFO, 'start bmp daemon') + subprocess.call(["service", "openbmpd", "start"]) + + +class BMPCfgDaemon: + def __init__(self): + self.state_db_conn = DBConnector(STATE_DB, 0) + self.config_db = ConfigDBConnector() + self.config_db.connect(wait_for_init=True, retry_on=True) + syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success') + self.bmpcfg = BMPCfg(self.state_db_conn) + + def bmp_handler(self, key, op, data): + syslog.syslog(syslog.LOG_INFO, 'BMP table handler...') + data = self.config_db.get_table("BMP") + self.bmpcfg.cfg_handler(data) + + def register_callbacks(self): + + def make_callback(func): + def callback(table, key, data): + if data is None: + op = "DEL" + data = {} + else: + op = "SET" + return func(key, op, data) + return callback + + self.config_db.subscribe('BMP', make_callback(self.bmp_handler)) + + def start(self): + self.config_db.listen(init_data_handler=self.load) + +def signal_handler(sig, frame): + if sig == signal.SIGHUP: + syslog.syslog(syslog.LOG_INFO, "bmpcfgd: signal 'SIGHUP' is caught and ignoring..") + elif sig == signal.SIGINT: + syslog.syslog(syslog.LOG_INFO, "bmpcfgd: signal 'SIGINT' is caught and exiting...") + sys.exit(128 + sig) + elif sig == signal.SIGTERM: + syslog.syslog(syslog.LOG_INFO, "bmpcfgd: signal 'SIGTERM' is caught and exiting...") + sys.exit(128 + sig) + else: + syslog.syslog(syslog.LOG_INFO, "bmpcfgd: invalid signal - ignoring..") + +def main(): + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGHUP, signal_handler) + daemon = BMPCfgDaemon() + daemon.register_callbacks() + daemon.start() + +if __name__ == "__main__": + main() diff --git a/tests/abmpcfgd_test.py b/tests/abmpcfgd_test.py new file mode 100644 index 00000000..c5dd24e7 --- /dev/null +++ b/tests/abmpcfgd_test.py @@ -0,0 +1,95 @@ +import importlib.machinery +import importlib.util +import filecmp +import json +import shutil +import os +import sys +from swsscommon import swsscommon + +from parameterized import parameterized +from unittest import TestCase, mock +from tests.common.mock_configdb import MockConfigDb, MockDBConnector +from tests.common.mock_bootloader import MockBootloader +from sonic_py_common.general import getstatusoutput_noshell + + +test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +modules_path = os.path.dirname(test_path) +scripts_path = os.path.join(modules_path, "scripts") +sys.path.insert(0, modules_path) + +# Load the file under test +bmpcfgd_path = os.path.join(scripts_path, 'bmpcfgd') +loader = importlib.machinery.SourceFileLoader('bmpcfgd', bmpcfgd_path) +spec = importlib.util.spec_from_loader(loader.name, loader) +bmpcfgd = importlib.util.module_from_spec(spec) +loader.exec_module(bmpcfgd) +sys.modules['bmpcfgd'] = bmpcfgd + + +original_syslog = bmpcfgd.syslog + +# Mock swsscommon classes +bmpcfgd.ConfigDBConnector = MockConfigDb +bmpcfgd.DBConnector = MockDBConnector +bmpcfgd.Table = mock.Mock() + + +class TestBMPCfgDaemon(TestCase): + """ + Test bmpcfgd daemon + """ + def setUp(self): + self.test_data['BMP']['table'] = {'bgp_neighbor_table': 'false', 'bgp_rib_in_table': 'false', 'bgp_rib_out_table': 'false'} + + @patch('subprocess.call') + def test_start_bmp(self, mock_call): + obj = BMPCfg() + obj.start_bmp() + mock_call.assert_called_once_with(["service", "openbmpd", "start"]) + + @patch('subprocess.call') + def test_stop_bmp(self, mock_call): + obj = BMPCfg() + obj.stop_bmp() + mock_call.assert_called_once_with(["service", "openbmpd", "stop"]) + + @mock.patch('sonic_installer.bootloader.get_bootloader', side_effect=[MockBootloader()]) + @mock.patch('syslog.syslog') + @mock.patch('subprocess.check_call') + def test_bmpcfgd_neighbor_enable(self, mock_check_call, mock_check_output, mock_syslog, mock_get_bootloader): + self.test_data['BMP']['table']['bgp_neighbor_table'] = 'true' + MockConfigDb.set_config_db(self.test_data) + bmp_config_daemon = bmpcfgd.BMPCfgDaemon() + bmp_config_daemon.bmp_handler("BMP", '', self.test_data) + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: update : true, false, false') + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: stop bmp daemon.') + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: Reset bmp table from state_db.') + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: start bmp daemon.') + + @mock.patch('sonic_installer.bootloader.get_bootloader', side_effect=[MockBootloader()]) + @mock.patch('syslog.syslog') + @mock.patch('subprocess.check_call') + def test_bmpcfgd_bgp_rib_in_enable(self, mock_check_call, mock_check_output, mock_syslog, mock_get_bootloader): + self.test_data['BMP']['table']['bgp_rib_in_table'] = 'true' + MockConfigDb.set_config_db(self.test_data) + bmp_config_daemon = bmpcfgd.BMPCfgDaemon() + bmp_config_daemon.bmp_handler("BMP", '', self.test_data) + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: update : false, true, false') + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: stop bmp daemon.') + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: Reset bmp table from state_db.') + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: start bmp daemon.') + + @mock.patch('sonic_installer.bootloader.get_bootloader', side_effect=[MockBootloader()]) + @mock.patch('syslog.syslog') + @mock.patch('subprocess.check_call') + def test_bmpcfgd_bgp_rib_out_enable(self, mock_check_call, mock_check_output, mock_syslog, mock_get_bootloader): + self.test_data['BMP']['table']['bgp_rib_out_table'] = 'true' + MockConfigDb.set_config_db(self.test_data) + bmp_config_daemon = bmpcfgd.BMPCfgDaemon() + bmp_config_daemon.bmp_handler("BMP", '', self.test_data) + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: update : false, false, true') + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: stop bmp daemon.') + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: Reset bmp table from state_db.') + mock_syslog.assert_any_call(original_syslog.LOG_INFO, 'BMPCfg: start bmp daemon.') \ No newline at end of file