Skip to content

Commit 1309065

Browse files
FengPan-FrankAidan Gallagher
authored and
Aidan Gallagher
committed
Add bmpcfgd for monitoring config_db state change. (sonic-net#18940)
#### Why I did it sonic-net/SONiC#1621, need dedicated daemon to monitor config_db and manage openbmpd state. #### How I did it Use swss-common lib to monitor config_db change.
1 parent 24f7a69 commit 1309065

File tree

9 files changed

+522
-0
lines changed

9 files changed

+522
-0
lines changed

src/sonic-bmpcfgd/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[Unit]
2+
Description=Process which monitors config_db and manage openbmpd daemon
3+
Requires=database.service config-setup.service
4+
After=database.service config-setup.service
5+
BindsTo=sonic.target
6+
After=sonic.target
7+
8+
[Service]
9+
Type=simple
10+
ExecStart=/usr/local/bin/bmpcfgd
11+
Restart=always
12+
13+
[Install]
14+
WantedBy=sonic.target

src/sonic-bmpcfgd/scripts/bmpcfgd

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
'''
3+
bmpcfgd
4+
Daemon which monitors bmp relevant table enablement from CONFIG_DB, and reset BMP states
5+
'''
6+
7+
import os
8+
import sys
9+
import subprocess
10+
import syslog
11+
import signal
12+
from shutil import copy2
13+
from datetime import datetime
14+
from sonic_py_common import device_info
15+
from sonic_py_common.general import check_output_pipe
16+
from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table
17+
from swsscommon import swsscommon
18+
from sonic_py_common.daemon_base import DaemonBase
19+
20+
CFG_DB = "CONFIG_DB"
21+
BMP_STATE_DB = "BMP_STATE_DB"
22+
REDIS_HOSTIP = "127.0.0.1"
23+
BMP_TABLE = "BMP"
24+
25+
def is_true(val):
26+
return str(val).lower() == 'true'
27+
28+
class BMPCfg(DaemonBase):
29+
def __init__(self, state_db_conn):
30+
DaemonBase.__init__(self, SYSLOG_IDENTIFIER)
31+
self.bgp_neighbor_table = False
32+
self.bgp_rib_in_table = False
33+
self.bgp_rib_out_table = False
34+
self.state_db_conn = state_db_conn
35+
36+
37+
def load(self, data={}):
38+
common_config = data.get('table', {})
39+
40+
self.bgp_neighbor_table = is_true(common_config.get('bgp_neighbor_table', 'false'))
41+
self.bgp_rib_in_table = is_true(common_config.get('bgp_rib_in_table', 'false'))
42+
self.bgp_rib_out_table = is_true(common_config.get('bgp_rib_out_table', 'false'))
43+
self.log_info(f'BMPCfg: update : {self.bgp_neighbor_table}, {self.bgp_rib_in_table}, {self.bgp_rib_out_table}')
44+
45+
# reset bmp table state once config is changed.
46+
self.stop_bmp()
47+
self.reset_bmp_table()
48+
self.start_bmp()
49+
50+
51+
def cfg_handler(self, data):
52+
self.load(data)
53+
54+
55+
def stop_bmp(self):
56+
self.log_info('BMPCfg: stop bmp daemon')
57+
subprocess.call(["service", "openbmpd", "stop"])
58+
59+
60+
def reset_bmp_table(self):
61+
self.log_info('BMPCfg: Reset bmp table from state_db')
62+
self.state_db_conn.delete_all_by_pattern(BMP_STATE_DB, 'BGP_NEIGHBOR*')
63+
self.state_db_conn.delete_all_by_pattern(BMP_STATE_DB, 'BGP_RIB_IN_TABLE*')
64+
self.state_db_conn.delete_all_by_pattern(BMP_STATE_DB, 'BGP_RIB_OUT_TABLE*')
65+
66+
67+
def start_bmp(self):
68+
self.log_info('BMPCfg: start bmp daemon')
69+
subprocess.call(["service", "openbmpd", "start"])
70+
71+
72+
class BMPCfgDaemon:
73+
def __init__(self):
74+
self.state_db_conn = swsscommon.SonicV2Connector(host=REDIS_HOSTIP)
75+
self.state_db_conn.connect(BMP_STATE_DB)
76+
self.config_db = ConfigDBConnector()
77+
self.config_db.connect(wait_for_init=True, retry_on=True)
78+
self.bmpcfg = BMPCfg(self.state_db_conn)
79+
80+
def bmp_handler(self, key, op, data):
81+
data = self.config_db.get_table(BMP_TABLE)
82+
self.bmpcfg.cfg_handler(data)
83+
84+
def register_callbacks(self):
85+
self.config_db.subscribe(BMP_TABLE,
86+
lambda table, key, data:
87+
self.bmp_handler(key, op, data))
88+
89+
def signal_handler(sig, frame):
90+
if sig == signal.SIGHUP:
91+
self.log_info("bmpcfgd: signal 'SIGHUP' is caught and ignoring..")
92+
elif sig == signal.SIGINT:
93+
self.log_info("bmpcfgd: signal 'SIGINT' is caught and exiting...")
94+
sys.exit(128 + sig)
95+
elif sig == signal.SIGTERM:
96+
self.log_info("bmpcfgd: signal 'SIGTERM' is caught and exiting...")
97+
sys.exit(128 + sig)
98+
else:
99+
self.log_info("bmpcfgd: invalid signal - ignoring..")
100+
101+
102+
def main():
103+
signal.signal(signal.SIGTERM, signal_handler)
104+
signal.signal(signal.SIGINT, signal_handler)
105+
signal.signal(signal.SIGHUP, signal_handler)
106+
daemon = BMPCfgDaemon()
107+
daemon.register_callbacks()
108+
109+
110+
if __name__ == "__main__":
111+
main()

src/sonic-bmpcfgd/setup.cfg

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[aliases]
2+
test=pytest
3+
[tool:pytest]
4+
addopts = --verbose
5+
python_files = tests/*.py

src/sonic-bmpcfgd/setup.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import print_function
2+
import sys
3+
from setuptools import setup
4+
import pkg_resources
5+
from packaging import version
6+
7+
# sonic_dependencies, version requirement only supports '>='
8+
sonic_dependencies = ['sonic-py-common', 'sonic-utilities']
9+
for package in sonic_dependencies:
10+
try:
11+
package_dist = pkg_resources.get_distribution(package.split(">=")[0])
12+
except pkg_resources.DistributionNotFound:
13+
print(package + " is not found!", file=sys.stderr)
14+
print("Please build and install SONiC python wheels dependencies from sonic-buildimage", file=sys.stderr)
15+
exit(1)
16+
if ">=" in package:
17+
if version.parse(package_dist.version) >= version.parse(package.split(">=")[1]):
18+
continue
19+
print(package + " version not match!", file=sys.stderr)
20+
exit(1)
21+
22+
setup(
23+
name = 'sonic-bmpcfgd-services',
24+
version = '1.0',
25+
description = 'Python services which run in the bmp container',
26+
license = 'Apache 2.0',
27+
author = 'SONiC Team',
28+
author_email = 'linuxnetdev@microsoft.com',
29+
url = 'https://github.com/Azure/sonic-buildimage',
30+
maintainer = 'Feng Pan',
31+
maintainer_email = 'fenpan@microsoft.com',
32+
packages = setuptools.find_packages(),
33+
scripts = [
34+
'scripts/bmpcfgd'
35+
],
36+
install_requires = [
37+
'jinja2>=2.10',
38+
'netaddr==0.8.0',
39+
'pyyaml==6.0.1',
40+
'ipaddress==1.0.23'
41+
] + sonic_dependencies,
42+
setup_requires = [
43+
'pytest-runner',
44+
'wheel'
45+
],
46+
tests_require = [
47+
'parameterized',
48+
'pytest',
49+
'pyfakefs',
50+
'sonic-py-common',
51+
'pytest-cov'
52+
],
53+
extras_require = {
54+
"testing": [
55+
'parameterized',
56+
'pytest',
57+
'pyfakefs',
58+
'sonic-py-common'
59+
]
60+
},
61+
classifiers = [
62+
'Development Status :: 3 - Alpha',
63+
'Environment :: Console',
64+
'Intended Audience :: Developers',
65+
'Intended Audience :: Information Technology',
66+
'Intended Audience :: System Administrators',
67+
'License :: OSI Approved :: Apache Software License',
68+
'Natural Language :: English',
69+
'Operating System :: POSIX :: Linux',
70+
'Programming Language :: Python :: 3.7',
71+
'Topic :: System',
72+
],
73+
keywords = 'sonic SONiC bmp services',
74+
test_suite = 'setup.get_test_suite'
75+
)

src/sonic-bmpcfgd/tests/__init__.py

Whitespace-only changes.
+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import importlib.machinery
2+
import importlib.util
3+
import filecmp
4+
import json
5+
import shutil
6+
import os
7+
import sys
8+
import signal
9+
from swsscommon import swsscommon
10+
11+
from parameterized import parameterized
12+
from unittest import TestCase, mock
13+
from tests.common.mock_configdb import MockConfigDb, MockDBConnector
14+
from tests.common.mock_bootloader import MockBootloader
15+
from sonic_py_common.general import getstatusoutput_noshell
16+
from .mock_connector import MockConnector
17+
from sonic_py_common.general import load_module_from_source
18+
from mock import patch
19+
20+
test_path = os.path.dirname(os.path.abspath(__file__))
21+
modules_path = os.path.dirname(test_path)
22+
scripts_path = os.path.join(modules_path, "scripts")
23+
sys.path.insert(0, modules_path)
24+
25+
# Load the file under test
26+
bmpcfgd_path = os.path.join(scripts_path, 'bmpcfgd')
27+
bmpcfgd = load_module_from_source('bmpcfgd', bmpcfgd_path)
28+
29+
30+
from bmpcfgd import signal_handler
31+
32+
original_syslog = bmpcfgd.syslog
33+
34+
# Mock swsscommon classes
35+
bmpcfgd.ConfigDBConnector = MockConfigDb
36+
bmpcfgd.DBConnector = MockDBConnector
37+
bmpcfgd.Table = mock.Mock()
38+
swsscommon.SonicV2Connector = MockConnector
39+
40+
class TestBMPCfgDaemon(TestCase):
41+
"""
42+
Test bmpcfgd daemon
43+
"""
44+
def setUp(self):
45+
self.test_data = {}
46+
self.test_data['BMP'] = {}
47+
self.test_data['BMP']['table'] = {'bgp_neighbor_table': 'false', 'bgp_rib_in_table': 'false', 'bgp_rib_out_table': 'false'}
48+
49+
@mock.patch('sonic_installer.bootloader.get_bootloader', side_effect=[MockBootloader()])
50+
@mock.patch('syslog.syslog')
51+
@mock.patch('subprocess.call')
52+
def test_bmpcfgd_neighbor_enable(self, mock_check_call, mock_syslog, mock_get_bootloader):
53+
self.test_data['BMP']['table']['bgp_neighbor_table'] = 'true'
54+
MockConfigDb.set_config_db(self.test_data)
55+
bmp_config_daemon = bmpcfgd.BMPCfgDaemon()
56+
bmp_config_daemon.register_callbacks()
57+
bmp_config_daemon.bmp_handler("BMP", '', self.test_data)
58+
expected_calls = [
59+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: update : True, False, False'),
60+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: stop bmp daemon'),
61+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: Reset bmp table from state_db'),
62+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: start bmp daemon'),
63+
]
64+
mock_syslog.assert_has_calls(expected_calls)
65+
66+
@mock.patch('sonic_installer.bootloader.get_bootloader', side_effect=[MockBootloader()])
67+
@mock.patch('syslog.syslog')
68+
@mock.patch('subprocess.check_call')
69+
def test_bmpcfgd_bgp_rib_in_enable(self, mock_check_call, mock_syslog, mock_get_bootloader):
70+
self.test_data['BMP']['table']['bgp_rib_in_table'] = 'true'
71+
MockConfigDb.set_config_db(self.test_data)
72+
bmp_config_daemon = bmpcfgd.BMPCfgDaemon()
73+
bmp_config_daemon.bmp_handler("BMP", '', self.test_data)
74+
expected_calls = [
75+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: update : False, True, False'),
76+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: stop bmp daemon'),
77+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: Reset bmp table from state_db'),
78+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: start bmp daemon'),
79+
]
80+
mock_syslog.assert_has_calls(expected_calls)
81+
82+
@mock.patch('sonic_installer.bootloader.get_bootloader', side_effect=[MockBootloader()])
83+
@mock.patch('syslog.syslog')
84+
@mock.patch('subprocess.check_call')
85+
def test_bmpcfgd_bgp_rib_out_enable(self, mock_check_call, mock_syslog, mock_get_bootloader):
86+
self.test_data['BMP']['table']['bgp_rib_out_table'] = 'true'
87+
MockConfigDb.set_config_db(self.test_data)
88+
bmp_config_daemon = bmpcfgd.BMPCfgDaemon()
89+
bmp_config_daemon.bmp_handler("BMP", '', self.test_data)
90+
expected_calls = [
91+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: update : False, False, True'),
92+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: stop bmp daemon'),
93+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: Reset bmp table from state_db'),
94+
mock.call(original_syslog.LOG_INFO, 'BMPCfg: start bmp daemon'),
95+
]
96+
mock_syslog.assert_has_calls(expected_calls)
97+
98+
99+
@mock.patch('syslog.syslog')
100+
@mock.patch.object(sys, 'exit')
101+
def test_signal_handler(self, mock_exit, mock_syslog):
102+
# Test SIGHUP signal
103+
signal_handler(signal.SIGHUP, None)
104+
mock_syslog.assert_called_with(original_syslog.LOG_INFO, "bmpcfgd: signal 'SIGHUP' is caught and ignoring..")
105+
mock_exit.assert_not_called()
106+
# Test SIGINT signal
107+
signal_handler(signal.SIGINT, None)
108+
mock_syslog.assert_called_with(original_syslog.LOG_INFO, "bmpcfgd: signal 'SIGINT' is caught and exiting...")
109+
mock_exit.assert_called_once_with(128 + signal.SIGINT)
110+
# Test SIGTERM signal
111+
signal_handler(signal.SIGTERM, None)
112+
mock_syslog.assert_called_with(original_syslog.LOG_INFO, "bmpcfgd: signal 'SIGTERM' is caught and exiting...")
113+
mock_exit.assert_called_with(128 + signal.SIGTERM)
114+
# Test invalid signal
115+
signal_handler(999, None)
116+
mock_syslog.assert_called_with(original_syslog.LOG_INFO, "bmpcfgd: invalid signal - ignoring..")

0 commit comments

Comments
 (0)