Skip to content

Commit ae613fe

Browse files
authored
[NTP] Update NTP configuration via ConfigDB (#60)
This PR brings functionality that allows us to update the NTP configs by writing to the NTP, NTP_SERVER, and NTP_KEY tables of ConfigDB. Signed-off-by: Yevhen Fastiuk <[email protected]>
1 parent beb8bbe commit ae613fe

File tree

3 files changed

+197
-77
lines changed

3 files changed

+197
-77
lines changed

scripts/hostcfgd

+133-66
Original file line numberDiff line numberDiff line change
@@ -982,84 +982,141 @@ class NtpCfg(object):
982982
1) ntp-config.service handles the configuration updates and then starts ntp.service
983983
2) Both of them start after all the feature services start
984984
3) Purpose of this daemon is to propagate runtime config changes in
985-
NTP, NTP_SERVER and LOOPBACK_INTERFACE
985+
NTP, NTP_SERVER, NTP_KEY, and LOOPBACK_INTERFACE
986986
"""
987+
NTP_CONF_RESTART = ['systemctl', 'restart', 'ntp-config']
988+
987989
def __init__(self):
988-
self.ntp_global = {}
989-
self.ntp_servers = set()
990+
self.cache = {}
991+
992+
def load(self, ntp_global_conf: dict, ntp_server_conf: dict,
993+
ntp_key_conf: dict):
994+
"""Load initial NTP configuration
990995
991-
def load(self, ntp_global_conf, ntp_server_conf):
992-
syslog.syslog(syslog.LOG_INFO, "NtpCfg load ...")
996+
Force load cache on init. NTP config should be taken at boot-time by
997+
ntp and ntp-config services. So loading whole config here.
998+
999+
Args:
1000+
ntp_global_conf: Global configuration
1001+
ntp_server_conf: Servers configuration
1002+
ntp_key_conf: Keys configuration
1003+
"""
9931004

994-
for row in ntp_global_conf:
995-
self.ntp_global_update(row, ntp_global_conf[row], is_load=True)
1005+
syslog.syslog(syslog.LOG_INFO, "NtpCfg: load initial")
9961006

997-
# Force reload on init
998-
self.ntp_server_update(0, None, is_load=True)
1007+
if ntp_global_conf is None:
1008+
ntp_global_conf = {}
1009+
1010+
# Force load cache on init.
1011+
# NTP config should be taken at boot-time by ntp and ntp-config
1012+
# services.
1013+
self.cache = {
1014+
'global': ntp_global_conf.get('global', {}),
1015+
'servers': ntp_server_conf,
1016+
'keys': ntp_key_conf
1017+
}
9991018

10001019
def handle_ntp_source_intf_chg(self, intf_name):
1001-
# if no ntp server configured, do nothing
1002-
if not self.ntp_servers:
1020+
# If no ntp server configured, do nothing. Source interface will be
1021+
# taken once any server will be configured.
1022+
if not self.cache.get('servers'):
10031023
return
10041024

10051025
# check only the intf configured as source interface
1006-
if intf_name not in self.ntp_global.get('src_intf', '').split(';'):
1026+
ifs = self.cache.get('global', {}).get('src_intf', '').split(';')
1027+
if intf_name not in ifs:
1028+
return
1029+
1030+
# Just restart ntp config
1031+
try:
1032+
run_cmd(self.NTP_CONF_RESTART, True, True)
1033+
except Exception:
1034+
syslog.syslog(syslog.LOG_ERR, 'NtpCfg: Failed to restart '
1035+
'ntp-config service')
1036+
return
1037+
1038+
def ntp_global_update(self, key: str, data: dict):
1039+
"""Update NTP global configuration
1040+
1041+
The table holds NTP global configuration. It has some configs that
1042+
require reloading only but some other require restarting ntp daemon.
1043+
Handle each of them accordingly.
1044+
1045+
Args:
1046+
key: Triggered table's key. Should be always "global"
1047+
data: Global configuration data
1048+
"""
1049+
1050+
syslog.syslog(syslog.LOG_NOTICE, 'NtpCfg: Global configuration update')
1051+
if key != 'global' or self.cache.get('global', {}) == data:
1052+
syslog.syslog(syslog.LOG_NOTICE, 'NtpCfg: Nothing to update')
10071053
return
1008-
else:
1009-
# just restart ntp config
1010-
cmd = ['systemctl', 'restart', 'ntp-config']
1011-
run_cmd(cmd)
10121054

1013-
def ntp_global_update(self, key, data, is_load=False):
1014-
syslog.syslog(syslog.LOG_INFO, 'NTP GLOBAL Update')
1015-
orig_src = self.ntp_global.get('src_intf', '')
1016-
orig_src_set = set(orig_src.split(";"))
1017-
orig_vrf = self.ntp_global.get('vrf', '')
1055+
syslog.syslog(syslog.LOG_INFO, f'NtpCfg: Set global config: {data}')
10181056

1019-
new_src = data.get('src_intf', '')
1020-
new_src_set = set(new_src.split(";"))
1021-
new_vrf = data.get('vrf', '')
1057+
old_dhcp = self.cache.get(key, {}).get('dhcp')
1058+
old_vrf = self.cache.get(key, {}).get('vrf')
1059+
new_dhcp = data.get('dhcp')
1060+
new_vrf = data.get('vrf')
1061+
1062+
restart_ntpd = False
1063+
if new_dhcp != old_dhcp or new_vrf != old_vrf:
1064+
restart_ntpd = True
1065+
1066+
# Restarting the service
1067+
try:
1068+
run_cmd(self.NTP_CONF_RESTART, True, True)
1069+
if restart_ntpd:
1070+
run_cmd(['systemctl', 'restart', 'ntp'], True, True)
1071+
except Exception:
1072+
syslog.syslog(syslog.LOG_ERR, f'NtpCfg: Failed to restart ntp '
1073+
'services')
1074+
return
10221075

10231076
# Update the Local Cache
1024-
self.ntp_global = data
1077+
self.cache[key] = data
1078+
1079+
def ntp_srv_key_update(self, ntp_servers: dict, ntp_keys: dict):
1080+
"""Update NTP server/key configuration
10251081
1026-
# If initial load don't restart daemon
1027-
if is_load: return
1082+
The tables holds only NTP servers config and/or NTP authentication keys
1083+
config, so any change to those tables should cause NTP config reload.
1084+
It does not make sense to handle each of the separately. NTP config
1085+
reload takes whole configuration, so once got to this handler - cache
1086+
whole config.
10281087
1029-
# check if ntp server configured, if not, do nothing
1030-
if not self.ntp_servers:
1031-
syslog.syslog(syslog.LOG_INFO, "No ntp server when global config change, do nothing")
1088+
Args:
1089+
ntp_servers: Servers config table
1090+
ntp_keys: Keys config table
1091+
"""
1092+
1093+
syslog.syslog(syslog.LOG_NOTICE, 'NtpCfg: Server/key configuration '
1094+
'update')
1095+
1096+
if (self.cache.get('servers', {}) == ntp_servers and
1097+
self.cache.get('keys', {}) == ntp_keys):
1098+
syslog.syslog(syslog.LOG_NOTICE, 'NtpCfg: Nothing to update')
10321099
return
10331100

1034-
if orig_src_set != new_src_set:
1035-
syslog.syslog(syslog.LOG_INFO, "ntp global update for source intf old {} new {}, restarting ntp-config"
1036-
.format(orig_src_set, new_src_set))
1037-
cmd = ['systemctl', 'restart', 'ntp-config']
1038-
run_cmd(cmd)
1039-
elif new_vrf != orig_vrf:
1040-
syslog.syslog(syslog.LOG_INFO, "ntp global update for vrf old {} new {}, restarting ntp service"
1041-
.format(orig_vrf, new_vrf))
1042-
cmd = ['service', 'ntp', 'restart']
1043-
run_cmd(cmd)
1101+
# Pop keys values to print
1102+
ntp_keys_print = copy.deepcopy(ntp_keys)
1103+
for key in ntp_keys_print:
1104+
ntp_keys_print[key].pop('value', None)
10441105

1045-
def ntp_server_update(self, key, op, is_load=False):
1046-
syslog.syslog(syslog.LOG_INFO, 'ntp server update key {}'.format(key))
1047-
1048-
restart_config = False
1049-
if not is_load:
1050-
if op == "SET" and key not in self.ntp_servers:
1051-
restart_config = True
1052-
self.ntp_servers.add(key)
1053-
elif op == "DEL" and key in self.ntp_servers:
1054-
restart_config = True
1055-
self.ntp_servers.remove(key)
1056-
else:
1057-
restart_config = True
1106+
syslog.syslog(syslog.LOG_INFO, f'NtpCfg: Set servers: {ntp_servers}')
1107+
syslog.syslog(syslog.LOG_INFO, f'NtpCfg: Set keys: {ntp_keys_print}')
10581108

1059-
if restart_config:
1060-
cmd = ['systemctl', 'restart', 'ntp-config']
1061-
syslog.syslog(syslog.LOG_INFO, 'ntp server update, restarting ntp-config, ntp servers configured {}'.format(self.ntp_servers))
1062-
run_cmd(cmd)
1109+
# Restarting the service
1110+
try:
1111+
run_cmd(self.NTP_CONF_RESTART, True, True)
1112+
except Exception:
1113+
syslog.syslog(syslog.LOG_ERR, f'NtpCfg: Failed to restart '
1114+
'ntp-config service')
1115+
return
1116+
1117+
# Updating the cache
1118+
self.cache['servers'] = ntp_servers
1119+
self.cache['keys'] = ntp_keys
10631120

10641121
class PamLimitsCfg(object):
10651122
"""
@@ -1501,8 +1558,6 @@ class HostConfigDaemon:
15011558
radius_global = init_data['RADIUS']
15021559
radius_server = init_data['RADIUS_SERVER']
15031560
lpbk_table = init_data['LOOPBACK_INTERFACE']
1504-
ntp_server = init_data['NTP_SERVER']
1505-
ntp_global = init_data['NTP']
15061561
kdump = init_data['KDUMP']
15071562
passwh = init_data['PASSW_HARDENING']
15081563
ssh_server = init_data['SSH_SERVER']
@@ -1513,10 +1568,12 @@ class HostConfigDaemon:
15131568
syslog_srv = init_data.get(swsscommon.CFG_SYSLOG_SERVER_TABLE_NAME, {})
15141569
dns = init_data.get('DNS_NAMESERVER', {})
15151570
fips_cfg = init_data.get('FIPS', {})
1571+
ntp_global = init_data.get(swsscommon.CFG_NTP_GLOBAL_TABLE_NAME)
1572+
ntp_servers = init_data.get(swsscommon.CFG_NTP_SERVER_TABLE_NAME)
1573+
ntp_keys = init_data.get(swsscommon.CFG_NTP_KEY_TABLE_NAME)
15161574

15171575
self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server)
15181576
self.iptables.load(lpbk_table)
1519-
self.ntpcfg.load(ntp_global, ntp_server)
15201577
self.kdumpCfg.load(kdump)
15211578
self.passwcfg.load(passwh)
15221579
self.sshscfg.load(ssh_server)
@@ -1526,6 +1583,7 @@ class HostConfigDaemon:
15261583
self.rsyslogcfg.load(syslog_cfg, syslog_srv)
15271584
self.dnscfg.load(dns)
15281585
self.fipscfg.load(fips_cfg)
1586+
self.ntpcfg.load(ntp_global, ntp_servers, ntp_keys)
15291587

15301588
# Update AAA with the hostname
15311589
self.aaacfg.hostname_update(self.devmetacfg.hostname)
@@ -1615,12 +1673,16 @@ class HostConfigDaemon:
16151673
key = ConfigDBConnector.deserialize_key(key)
16161674
self.aaacfg.handle_radius_source_intf_ip_chg(key)
16171675

1618-
def ntp_server_handler(self, key, op, data):
1619-
self.ntpcfg.ntp_server_update(key, op)
1620-
16211676
def ntp_global_handler(self, key, op, data):
1677+
syslog.syslog(syslog.LOG_NOTICE, 'Handling NTP global config')
16221678
self.ntpcfg.ntp_global_update(key, data)
16231679

1680+
def ntp_srv_key_handler(self, key, op, data):
1681+
syslog.syslog(syslog.LOG_NOTICE, 'Handling NTP server/key config')
1682+
self.ntpcfg.ntp_srv_key_update(
1683+
self.config_db.get_table(swsscommon.CFG_NTP_SERVER_TABLE_NAME),
1684+
self.config_db.get_table(swsscommon.CFG_NTP_KEY_TABLE_NAME))
1685+
16241686
def kdump_handler (self, key, op, data):
16251687
syslog.syslog(syslog.LOG_INFO, 'Kdump handler...')
16261688
self.kdumpCfg.kdump_update(key, data)
@@ -1682,9 +1744,6 @@ class HostConfigDaemon:
16821744
self.config_db.subscribe('SSH_SERVER', make_callback(self.ssh_handler))
16831745
# Handle IPTables configuration
16841746
self.config_db.subscribe('LOOPBACK_INTERFACE', make_callback(self.lpbk_handler))
1685-
# Handle NTP & NTP_SERVER updates
1686-
self.config_db.subscribe('NTP', make_callback(self.ntp_global_handler))
1687-
self.config_db.subscribe('NTP_SERVER', make_callback(self.ntp_server_handler))
16881747
# Handle updates to src intf changes in radius
16891748
self.config_db.subscribe('MGMT_INTERFACE', make_callback(self.mgmt_intf_handler))
16901749
self.config_db.subscribe('VLAN_INTERFACE', make_callback(self.vlan_intf_handler))
@@ -1711,6 +1770,14 @@ class HostConfigDaemon:
17111770
# Handle FIPS changes
17121771
self.config_db.subscribe('FIPS', make_callback(self.fips_config_handler))
17131772

1773+
# Handle NTP, NTP_SERVER, and NTP_KEY updates
1774+
self.config_db.subscribe(swsscommon.CFG_NTP_GLOBAL_TABLE_NAME,
1775+
make_callback(self.ntp_global_handler))
1776+
self.config_db.subscribe(swsscommon.CFG_NTP_SERVER_TABLE_NAME,
1777+
make_callback(self.ntp_srv_key_handler))
1778+
self.config_db.subscribe(swsscommon.CFG_NTP_KEY_TABLE_NAME,
1779+
make_callback(self.ntp_srv_key_handler))
1780+
17141781
syslog.syslog(syslog.LOG_INFO,
17151782
"Waiting for systemctl to finish initialization")
17161783
self.wait_till_system_init_done()

tests/hostcfgd/hostcfgd_test.py

+53-11
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,35 @@ class TesNtpCfgd(TestCase):
3434
Test hostcfd daemon - NtpCfgd
3535
"""
3636
def setUp(self):
37-
MockConfigDb.CONFIG_DB['NTP'] = {'global': {'vrf': 'mgmt', 'src_intf': 'eth0'}}
37+
MockConfigDb.CONFIG_DB['NTP'] = {
38+
'global': {'vrf': 'mgmt', 'src_intf': 'eth0'}
39+
}
3840
MockConfigDb.CONFIG_DB['NTP_SERVER'] = {'0.debian.pool.ntp.org': {}}
41+
MockConfigDb.CONFIG_DB['NTP_KEY'] = {'42': {'value': 'theanswer'}}
3942

4043
def tearDown(self):
4144
MockConfigDb.CONFIG_DB = {}
4245

43-
def test_ntp_global_update_with_no_servers(self):
46+
def test_ntp_update_ntp_keys(self):
4447
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
4548
popen_mock = mock.Mock()
4649
attrs = {'communicate.return_value': ('output', 'error')}
4750
popen_mock.configure_mock(**attrs)
4851
mocked_subprocess.Popen.return_value = popen_mock
4952

5053
ntpcfgd = hostcfgd.NtpCfg()
51-
ntpcfgd.ntp_global_update('global', MockConfigDb.CONFIG_DB['NTP']['global'])
52-
53-
mocked_subprocess.check_call.assert_not_called()
54+
ntpcfgd.ntp_global_update(
55+
'global', MockConfigDb.CONFIG_DB['NTP']['global'])
56+
mocked_subprocess.check_call.assert_has_calls([
57+
call(['systemctl', 'restart', 'ntp-config']),
58+
call(['systemctl', 'restart', 'ntp'])
59+
])
60+
61+
mocked_subprocess.check_call.reset_mock()
62+
ntpcfgd.ntp_srv_key_update({}, MockConfigDb.CONFIG_DB['NTP_KEY'])
63+
mocked_subprocess.check_call.assert_has_calls([
64+
call(['systemctl', 'restart', 'ntp-config'])
65+
])
5466

5567
def test_ntp_global_update_ntp_servers(self):
5668
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
@@ -60,9 +72,37 @@ def test_ntp_global_update_ntp_servers(self):
6072
mocked_subprocess.Popen.return_value = popen_mock
6173

6274
ntpcfgd = hostcfgd.NtpCfg()
63-
ntpcfgd.ntp_global_update('global', MockConfigDb.CONFIG_DB['NTP']['global'])
64-
ntpcfgd.ntp_server_update('0.debian.pool.ntp.org', 'SET')
65-
mocked_subprocess.check_call.assert_has_calls([call(['systemctl', 'restart', 'ntp-config'])])
75+
ntpcfgd.ntp_global_update(
76+
'global', MockConfigDb.CONFIG_DB['NTP']['global'])
77+
mocked_subprocess.check_call.assert_has_calls([
78+
call(['systemctl', 'restart', 'ntp-config']),
79+
call(['systemctl', 'restart', 'ntp'])
80+
])
81+
82+
mocked_subprocess.check_call.reset_mock()
83+
ntpcfgd.ntp_srv_key_update({'0.debian.pool.ntp.org': {}}, {})
84+
mocked_subprocess.check_call.assert_has_calls([
85+
call(['systemctl', 'restart', 'ntp-config'])
86+
])
87+
88+
def test_ntp_is_caching_config(self):
89+
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
90+
popen_mock = mock.Mock()
91+
attrs = {'communicate.return_value': ('output', 'error')}
92+
popen_mock.configure_mock(**attrs)
93+
mocked_subprocess.Popen.return_value = popen_mock
94+
95+
ntpcfgd = hostcfgd.NtpCfg()
96+
ntpcfgd.cache['global'] = MockConfigDb.CONFIG_DB['NTP']['global']
97+
ntpcfgd.cache['servers'] = MockConfigDb.CONFIG_DB['NTP_SERVER']
98+
ntpcfgd.cache['keys'] = MockConfigDb.CONFIG_DB['NTP_KEY']
99+
100+
ntpcfgd.ntp_global_update(
101+
'global', MockConfigDb.CONFIG_DB['NTP']['global'])
102+
ntpcfgd.ntp_srv_key_update(MockConfigDb.CONFIG_DB['NTP_SERVER'],
103+
MockConfigDb.CONFIG_DB['NTP_KEY'])
104+
105+
mocked_subprocess.check_call.assert_not_called()
66106

67107
def test_loopback_update(self):
68108
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
@@ -72,11 +112,13 @@ def test_loopback_update(self):
72112
mocked_subprocess.Popen.return_value = popen_mock
73113

74114
ntpcfgd = hostcfgd.NtpCfg()
75-
ntpcfgd.ntp_global = MockConfigDb.CONFIG_DB['NTP']['global']
76-
ntpcfgd.ntp_servers.add('0.debian.pool.ntp.org')
115+
ntpcfgd.cache['global'] = MockConfigDb.CONFIG_DB['NTP']['global']
116+
ntpcfgd.cache['servers'] = {'0.debian.pool.ntp.org': {}}
77117

78118
ntpcfgd.handle_ntp_source_intf_chg('eth0')
79-
mocked_subprocess.check_call.assert_has_calls([call(['systemctl', 'restart', 'ntp-config'])])
119+
mocked_subprocess.check_call.assert_has_calls([
120+
call(['systemctl', 'restart', 'ntp-config'])
121+
])
80122

81123

82124
class TestHostcfgdDaemon(TestCase):

tests/hostcfgd/test_vectors.py

+11
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@
5252
"NTP_SERVER": {
5353
"0.debian.pool.ntp.org": {}
5454
},
55+
"NTP_KEY": {
56+
"1": {
57+
"value": "blahblah",
58+
"type": "md5"
59+
},
60+
"42": {
61+
"value": "theanswer",
62+
"type": "md5",
63+
"trusted": "yes"
64+
}
65+
},
5566
"LOOPBACK_INTERFACE": {
5667
"Loopback0|10.184.8.233/32": {
5768
"scope": "global",

0 commit comments

Comments
 (0)