[hostcfgd] [202106] Fixed the brief blackout in hostcfgd using SubscriberStateTable (#9031)
#### Why I did it Ported https://github.com/Azure/sonic-buildimage/pull/8861 to 202106 as it couldn't be cherry-pick directly
This commit is contained in:
parent
b125f5d564
commit
91628135e9
@ -4,12 +4,15 @@ import ast
|
|||||||
import copy
|
import copy
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import syslog
|
import syslog
|
||||||
|
import signal
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
from sonic_py_common import device_info
|
from sonic_py_common import device_info
|
||||||
from swsscommon.swsscommon import ConfigDBConnector
|
from swsscommon.swsscommon import SubscriberStateTable, DBConnector, Select
|
||||||
|
from swsscommon.swsscommon import ConfigDBConnector, TableConsumable
|
||||||
|
|
||||||
# FILE
|
# FILE
|
||||||
PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
|
PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
|
||||||
@ -36,6 +39,33 @@ RADIUS_SERVER_TIMEOUT_DEFAULT = "5"
|
|||||||
RADIUS_SERVER_AUTH_TYPE_DEFAULT = "pap"
|
RADIUS_SERVER_AUTH_TYPE_DEFAULT = "pap"
|
||||||
RADIUS_PAM_AUTH_CONF_DIR = "/etc/pam_radius_auth.d/"
|
RADIUS_PAM_AUTH_CONF_DIR = "/etc/pam_radius_auth.d/"
|
||||||
|
|
||||||
|
# MISC Constants
|
||||||
|
CFG_DB = "CONFIG_DB"
|
||||||
|
HOSTCFGD_MAX_PRI = 10 # Used to enforce ordering b/w daemons under Hostcfgd
|
||||||
|
DEFAULT_SELECT_TIMEOUT = 1000
|
||||||
|
|
||||||
|
|
||||||
|
def safe_eval(val, default_value=False):
|
||||||
|
""" Safely evaluate the expression, without raising an exception """
|
||||||
|
try:
|
||||||
|
ret = ast.literal_eval(val)
|
||||||
|
except ValueError:
|
||||||
|
ret = default_value
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
if sig == signal.SIGHUP:
|
||||||
|
syslog.syslog(syslog.LOG_INFO, "HostCfgd: signal 'SIGHUP' is caught and ignoring..")
|
||||||
|
elif sig == signal.SIGINT:
|
||||||
|
syslog.syslog(syslog.LOG_INFO, "HostCfgd: signal 'SIGINT' is caught and exiting...")
|
||||||
|
sys.exit(128 + sig)
|
||||||
|
elif sig == signal.SIGTERM:
|
||||||
|
syslog.syslog(syslog.LOG_INFO, "HostCfgd: signal 'SIGTERM' is caught and exiting...")
|
||||||
|
sys.exit(128 + sig)
|
||||||
|
else:
|
||||||
|
syslog.syslog(syslog.LOG_INFO, "HostCfgd: invalid signal - ignoring..")
|
||||||
|
|
||||||
|
|
||||||
def run_cmd(cmd, log_err=True, raise_exception=False):
|
def run_cmd(cmd, log_err=True, raise_exception=False):
|
||||||
try:
|
try:
|
||||||
@ -86,9 +116,9 @@ class Feature(object):
|
|||||||
self.name = feature_name
|
self.name = feature_name
|
||||||
self.state = self._get_target_state(feature_cfg.get('state'), device_config or {})
|
self.state = self._get_target_state(feature_cfg.get('state'), device_config or {})
|
||||||
self.auto_restart = feature_cfg.get('auto_restart', 'disabled')
|
self.auto_restart = feature_cfg.get('auto_restart', 'disabled')
|
||||||
self.has_timer = ast.literal_eval(feature_cfg.get('has_timer', 'False'))
|
self.has_timer = safe_eval(feature_cfg.get('has_timer', 'False'))
|
||||||
self.has_global_scope = ast.literal_eval(feature_cfg.get('has_global_scope', 'True'))
|
self.has_global_scope = safe_eval(feature_cfg.get('has_global_scope', 'True'))
|
||||||
self.has_per_asic_scope = ast.literal_eval(feature_cfg.get('has_per_asic_scope', 'False'))
|
self.has_per_asic_scope = safe_eval(feature_cfg.get('has_per_asic_scope', 'False'))
|
||||||
|
|
||||||
def _get_target_state(self, state_configuration, device_config):
|
def _get_target_state(self, state_configuration, device_config):
|
||||||
""" Returns the target state for the feature by rendering the state field as J2 template.
|
""" Returns the target state for the feature by rendering the state field as J2 template.
|
||||||
@ -109,6 +139,14 @@ class Feature(object):
|
|||||||
raise ValueError('Invalid state rendered for feature {}: {}'.format(self.name, target_state))
|
raise ValueError('Invalid state rendered for feature {}: {}'.format(self.name, target_state))
|
||||||
return target_state
|
return target_state
|
||||||
|
|
||||||
|
def compare_state(self, feature_name, feature_cfg):
|
||||||
|
if self.name != feature_name or not isinstance(feature_cfg, dict):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.state != feature_cfg.get('state', ''):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class FeatureHandler(object):
|
class FeatureHandler(object):
|
||||||
""" Handles FEATURE table updates. """
|
""" Handles FEATURE table updates. """
|
||||||
@ -122,7 +160,7 @@ class FeatureHandler(object):
|
|||||||
self._cached_config = {}
|
self._cached_config = {}
|
||||||
self.is_multi_npu = device_info.is_multi_npu()
|
self.is_multi_npu = device_info.is_multi_npu()
|
||||||
|
|
||||||
def handle(self, feature_name, feature_cfg):
|
def handle(self, feature_name, op, feature_cfg):
|
||||||
if not feature_cfg:
|
if not feature_cfg:
|
||||||
self._cached_config.pop(feature_name)
|
self._cached_config.pop(feature_name)
|
||||||
syslog.syslog(syslog.LOG_INFO, "Deregistering feature {}".format(feature_name))
|
syslog.syslog(syslog.LOG_INFO, "Deregistering feature {}".format(feature_name))
|
||||||
@ -136,9 +174,7 @@ class FeatureHandler(object):
|
|||||||
# the next called self.update_feature_state will start it again. If it will fail
|
# the next called self.update_feature_state will start it again. If it will fail
|
||||||
# again the auto restart will kick-in. Another order may leave it in failed state
|
# again the auto restart will kick-in. Another order may leave it in failed state
|
||||||
# and not auto restart.
|
# and not auto restart.
|
||||||
if self._cached_config[feature_name].auto_restart != feature.auto_restart:
|
self.update_feature_auto_restart(feature, feature_name)
|
||||||
self.update_feature_auto_restart(feature)
|
|
||||||
self._cached_config[feature_name].auto_restart = feature.auto_restart
|
|
||||||
|
|
||||||
# Enable/disable the container service if the feature state was changed from its previous state.
|
# Enable/disable the container service if the feature state was changed from its previous state.
|
||||||
if self._cached_config[feature_name].state != feature.state:
|
if self._cached_config[feature_name].state != feature.state:
|
||||||
@ -147,7 +183,12 @@ class FeatureHandler(object):
|
|||||||
else:
|
else:
|
||||||
self.resync_feature_state(self._cached_config[feature_name])
|
self.resync_feature_state(self._cached_config[feature_name])
|
||||||
|
|
||||||
def update_all_features_config(self):
|
def sync_state_field(self):
|
||||||
|
"""
|
||||||
|
Summary:
|
||||||
|
Updates the state field in the FEATURE|* tables as the state field
|
||||||
|
might have to be rendered based on DEVICE_METADATA table
|
||||||
|
"""
|
||||||
feature_table = self._config_db.get_table('FEATURE')
|
feature_table = self._config_db.get_table('FEATURE')
|
||||||
for feature_name in feature_table.keys():
|
for feature_name in feature_table.keys():
|
||||||
if not feature_name:
|
if not feature_name:
|
||||||
@ -155,12 +196,8 @@ class FeatureHandler(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
feature = Feature(feature_name, feature_table[feature_name], self._device_config)
|
feature = Feature(feature_name, feature_table[feature_name], self._device_config)
|
||||||
self._cached_config.setdefault(feature_name, feature)
|
if not feature.compare_state(feature_name, feature_table.get(feature_name, {})):
|
||||||
|
self.resync_feature_state(feature)
|
||||||
self.update_feature_auto_restart(feature)
|
|
||||||
|
|
||||||
self.update_feature_state(feature)
|
|
||||||
self.resync_feature_state(feature)
|
|
||||||
|
|
||||||
def update_feature_state(self, feature):
|
def update_feature_state(self, feature):
|
||||||
cached_feature = self._cached_config[feature.name]
|
cached_feature = self._cached_config[feature.name]
|
||||||
@ -203,16 +240,33 @@ class FeatureHandler(object):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def update_feature_auto_restart(self, feature):
|
def update_feature_auto_restart(self, feature, feature_name):
|
||||||
|
|
||||||
|
dir_name = self.SYSTEMD_SERVICE_CONF_DIR.format(feature_name)
|
||||||
|
auto_restart_conf = os.path.join(dir_name, 'auto_restart.conf')
|
||||||
|
|
||||||
|
write_conf = False
|
||||||
|
if not os.path.exists(auto_restart_conf): # if the auto_restart_conf file is not found, set it
|
||||||
|
write_conf = True
|
||||||
|
|
||||||
|
if self._cached_config[feature_name].auto_restart != feature.auto_restart:
|
||||||
|
write_conf = True
|
||||||
|
|
||||||
|
if not write_conf:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._cached_config[feature_name].auto_restart = feature.auto_restart # Update Cache
|
||||||
|
|
||||||
restart_config = "always" if feature.auto_restart == "enabled" else "no"
|
restart_config = "always" if feature.auto_restart == "enabled" else "no"
|
||||||
service_conf = "[Service]\nRestart={}\n".format(restart_config)
|
service_conf = "[Service]\nRestart={}\n".format(restart_config)
|
||||||
feature_names, feature_suffixes = self.get_feature_attribute(feature)
|
feature_names, feature_suffixes = self.get_feature_attribute(feature)
|
||||||
|
|
||||||
for feature_name in feature_names:
|
for name in feature_names:
|
||||||
dir_name = self.SYSTEMD_SERVICE_CONF_DIR.format(feature_name)
|
dir_name = self.SYSTEMD_SERVICE_CONF_DIR.format(name)
|
||||||
|
auto_restart_conf = os.path.join(dir_name, 'auto_restart.conf')
|
||||||
if not os.path.exists(dir_name):
|
if not os.path.exists(dir_name):
|
||||||
os.mkdir(dir_name)
|
os.mkdir(dir_name)
|
||||||
with open(os.path.join(dir_name, 'auto_restart.conf'), 'w') as cfgfile:
|
with open(auto_restart_conf, 'w') as cfgfile:
|
||||||
cfgfile.write(service_conf)
|
cfgfile.write(service_conf)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -250,7 +304,6 @@ class FeatureHandler(object):
|
|||||||
props = dict([line.split("=") for line in stdout.decode().strip().splitlines()])
|
props = dict([line.split("=") for line in stdout.decode().strip().splitlines()])
|
||||||
return props["UnitFileState"]
|
return props["UnitFileState"]
|
||||||
|
|
||||||
|
|
||||||
def enable_feature(self, feature):
|
def enable_feature(self, feature):
|
||||||
cmds = []
|
cmds = []
|
||||||
feature_names, feature_suffixes = self.get_feature_attribute(feature)
|
feature_names, feature_suffixes = self.get_feature_attribute(feature)
|
||||||
@ -321,10 +374,6 @@ class Iptables(object):
|
|||||||
'''
|
'''
|
||||||
return (isinstance(key, tuple))
|
return (isinstance(key, tuple))
|
||||||
|
|
||||||
def load(self, lpbk_table):
|
|
||||||
for row in lpbk_table:
|
|
||||||
self.iptables_handler(row, lpbk_table[row])
|
|
||||||
|
|
||||||
def command(self, chain, ip, ver, op):
|
def command(self, chain, ip, ver, op):
|
||||||
cmd = 'iptables' if ver == '4' else 'ip6tables'
|
cmd = 'iptables' if ver == '4' else 'ip6tables'
|
||||||
cmd += ' -t mangle --{} {} -p tcp --tcp-flags SYN SYN'.format(op, chain)
|
cmd += ' -t mangle --{} {} -p tcp --tcp-flags SYN SYN'.format(op, chain)
|
||||||
@ -375,11 +424,8 @@ class Iptables(object):
|
|||||||
|
|
||||||
for cmd in iptables_cmds:
|
for cmd in iptables_cmds:
|
||||||
syslog.syslog(syslog.LOG_INFO, "Running cmd - {}".format(cmd))
|
syslog.syslog(syslog.LOG_INFO, "Running cmd - {}".format(cmd))
|
||||||
try:
|
run_cmd(cmd)
|
||||||
subprocess.check_call(cmd, shell=True)
|
|
||||||
except subprocess.CalledProcessError as err:
|
|
||||||
syslog.syslog(syslog.LOG_ERR, "'{}' failed. RC: {}, output: {}"
|
|
||||||
.format(err.cmd, err.returncode, err.output))
|
|
||||||
|
|
||||||
class AaaCfg(object):
|
class AaaCfg(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -719,6 +765,7 @@ class AaaCfg(object):
|
|||||||
"{} - failed: return code - {}, output:\n{}"
|
"{} - failed: return code - {}, output:\n{}"
|
||||||
.format(err.cmd, err.returncode, err.output))
|
.format(err.cmd, err.returncode, err.output))
|
||||||
|
|
||||||
|
|
||||||
class KdumpCfg(object):
|
class KdumpCfg(object):
|
||||||
def __init__(self, CfgDb):
|
def __init__(self, CfgDb):
|
||||||
self.config_db = CfgDb
|
self.config_db = CfgDb
|
||||||
@ -727,19 +774,17 @@ class KdumpCfg(object):
|
|||||||
"num_dumps": "3" }
|
"num_dumps": "3" }
|
||||||
|
|
||||||
def load(self, kdump_table):
|
def load(self, kdump_table):
|
||||||
syslog.syslog(syslog.LOG_INFO, "KdumpCfg load ...")
|
"""
|
||||||
data = {}
|
Set the KDUMP table in CFG DB to kdump_defaults if not set by the user
|
||||||
|
"""
|
||||||
|
syslog.syslog(syslog.LOG_INFO, "KdumpCfg init ...")
|
||||||
kdump_conf = kdump_table.get("config", {})
|
kdump_conf = kdump_table.get("config", {})
|
||||||
for row in self.kdump_defaults:
|
for row in self.kdump_defaults:
|
||||||
value = self.kdump_defaults.get(row)
|
value = self.kdump_defaults.get(row)
|
||||||
if kdump_conf.get(row) is not None:
|
if not kdump_conf.get(row):
|
||||||
value = kdump_conf.get(row)
|
self.config_db.mod_entry("KDUMP", "config", {row : value})
|
||||||
else:
|
|
||||||
self.config_db.mod_entry("KDUMP", "config", { row : value})
|
|
||||||
data[row] = value
|
|
||||||
self.kdump_update("config", data, True)
|
|
||||||
|
|
||||||
def kdump_update(self, key, data, isLoad):
|
def kdump_update(self, key, data):
|
||||||
syslog.syslog(syslog.LOG_INFO, "Kdump global configuration update")
|
syslog.syslog(syslog.LOG_INFO, "Kdump global configuration update")
|
||||||
if key == "config":
|
if key == "config":
|
||||||
# Admin mode
|
# Admin mode
|
||||||
@ -759,136 +804,124 @@ class KdumpCfg(object):
|
|||||||
memory = self.kdump_defaults["memory"]
|
memory = self.kdump_defaults["memory"]
|
||||||
if data.get("memory") is not None:
|
if data.get("memory") is not None:
|
||||||
memory = data.get("memory")
|
memory = data.get("memory")
|
||||||
if isLoad or data.get("memory") is not None:
|
if data.get("memory") is not None:
|
||||||
run_cmd("sonic-kdump-config --memory " + memory)
|
run_cmd("sonic-kdump-config --memory " + memory)
|
||||||
|
|
||||||
# Num dumps
|
# Num dumps
|
||||||
num_dumps = self.kdump_defaults["num_dumps"]
|
num_dumps = self.kdump_defaults["num_dumps"]
|
||||||
if data.get("num_dumps") is not None:
|
if data.get("num_dumps") is not None:
|
||||||
num_dumps = data.get("num_dumps")
|
num_dumps = data.get("num_dumps")
|
||||||
if isLoad or data.get("num_dumps") is not None:
|
if data.get("num_dumps") is not None:
|
||||||
run_cmd("sonic-kdump-config --num_dumps " + num_dumps)
|
run_cmd("sonic-kdump-config --num_dumps " + num_dumps)
|
||||||
|
|
||||||
class NtpCfg(object):
|
class NtpCfg(object):
|
||||||
def __init__(self, CfgDb):
|
"""
|
||||||
self.config_db = CfgDb
|
NtpCfg Config Daemon
|
||||||
|
1) ntp-config.service handles the configuration updates and then starts ntp.service
|
||||||
|
2) Both of them start after all the feature services start
|
||||||
|
3) Purpose of this daemon is to propagate runtime config changes in
|
||||||
|
NTP, NTP_SERVER and LOOPBACK_INTERFACE
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
self.ntp_global = {}
|
self.ntp_global = {}
|
||||||
self.has_ntp_servers = False
|
self.ntp_servers = set()
|
||||||
|
|
||||||
def load(self, ntp_global_conf, ntp_server_conf):
|
def handle_ntp_source_intf_chg(self, intf_name):
|
||||||
syslog.syslog(syslog.LOG_INFO, "NtpCfg load ...")
|
|
||||||
|
|
||||||
for row in ntp_global_conf:
|
|
||||||
self.ntp_global_update(row, ntp_global_conf[row], True)
|
|
||||||
|
|
||||||
self.ntp_server_update(0, ntp_server_conf, True)
|
|
||||||
|
|
||||||
def handle_ntp_source_intf_chg (self, key):
|
|
||||||
# if no ntp server configured, do nothing
|
# if no ntp server configured, do nothing
|
||||||
if self.has_ntp_servers == False:
|
if not self.ntp_servers:
|
||||||
return
|
return
|
||||||
|
|
||||||
# check only the intf configured as source interface
|
# check only the intf configured as source interface
|
||||||
if (len(self.ntp_global) == 0):
|
if intf_name not in self.ntp_global.get('src_intf', '').split(';'):
|
||||||
return
|
|
||||||
|
|
||||||
if 'src_intf' not in self.ntp_global:
|
|
||||||
return
|
|
||||||
|
|
||||||
if key[0] != self.ntp_global['src_intf']:
|
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# just restart ntp config
|
# just restart ntp config
|
||||||
cmd = 'systemctl restart ntp-config'
|
cmd = 'systemctl restart ntp-config'
|
||||||
run_cmd(cmd)
|
run_cmd(cmd)
|
||||||
|
|
||||||
def ntp_global_update(self, key, data, isLoad):
|
def ntp_global_update(self, key, data):
|
||||||
syslog.syslog(syslog.LOG_INFO, "ntp global configuration update")
|
syslog.syslog(syslog.LOG_INFO, 'NTP GLOBAL Update')
|
||||||
|
orig_src = self.ntp_global.get('src_intf', '')
|
||||||
new_src = new_vrf = orig_src = orig_vrf = ""
|
orig_src_set = set(orig_src.split(";"))
|
||||||
|
orig_vrf = self.ntp_global.get('vrf', '')
|
||||||
if 'src_intf' in data:
|
|
||||||
new_src = data['src_intf']
|
|
||||||
|
|
||||||
if 'vrf' in data:
|
|
||||||
new_vrf = data['vrf']
|
|
||||||
|
|
||||||
if (len(self.ntp_global) != 0):
|
|
||||||
|
|
||||||
if 'src_intf' in self.ntp_global:
|
|
||||||
orig_src = self.ntp_global['src_intf']
|
|
||||||
|
|
||||||
if 'vrf' in self.ntp_global:
|
|
||||||
orig_vrf = self.ntp_global['vrf']
|
|
||||||
|
|
||||||
|
new_src = data.get('src_intf', '')
|
||||||
|
new_src_set = set(new_src.split(";"))
|
||||||
|
new_vrf = data.get('vrf', '')
|
||||||
|
|
||||||
|
# Update the Local Cache
|
||||||
self.ntp_global = data
|
self.ntp_global = data
|
||||||
|
|
||||||
# during initial load of ntp configuration, ntp server configuration decides if to restart ntp-config
|
|
||||||
if (isLoad):
|
|
||||||
syslog.syslog(syslog.LOG_INFO, "ntp global update in load")
|
|
||||||
return
|
|
||||||
|
|
||||||
# check if ntp server configured, if not, do nothing
|
# check if ntp server configured, if not, do nothing
|
||||||
if self.has_ntp_servers == False:
|
if not self.ntp_servers:
|
||||||
syslog.syslog(syslog.LOG_INFO, "no ntp server when global config change, do nothing")
|
syslog.syslog(syslog.LOG_INFO, "No ntp server when global config change, do nothing")
|
||||||
return
|
return
|
||||||
|
|
||||||
if (new_src != orig_src):
|
if orig_src_set != new_src_set:
|
||||||
syslog.syslog(syslog.LOG_INFO, "ntp global update for source intf old {} new {}, restarting ntp-config"
|
syslog.syslog(syslog.LOG_INFO, "ntp global update for source intf old {} new {}, restarting ntp-config"
|
||||||
.format(orig_src, new_src))
|
.format(orig_src_set, new_src_set))
|
||||||
cmd = 'systemctl restart ntp-config'
|
cmd = 'systemctl restart ntp-config'
|
||||||
run_cmd(cmd)
|
run_cmd(cmd)
|
||||||
else:
|
elif new_vrf != orig_vrf:
|
||||||
if (new_vrf != orig_vrf):
|
syslog.syslog(syslog.LOG_INFO, "ntp global update for vrf old {} new {}, restarting ntp service"
|
||||||
syslog.syslog(syslog.LOG_INFO, "ntp global update for vrf old {} new {}, restarting ntp service"
|
.format(orig_vrf, new_vrf))
|
||||||
.format(orig_vrf, new_vrf))
|
cmd = 'service ntp restart'
|
||||||
cmd = 'service ntp restart'
|
run_cmd(cmd)
|
||||||
run_cmd(cmd)
|
|
||||||
|
|
||||||
def ntp_server_update(self, key, data, isLoad):
|
def ntp_server_update(self, key, op):
|
||||||
syslog.syslog(syslog.LOG_INFO, 'ntp server update key {} data {}'.format(key, data))
|
syslog.syslog(syslog.LOG_INFO, 'ntp server update key {}'.format(key))
|
||||||
|
|
||||||
# during load, restart ntp-config regardless if ntp server is configured or not
|
restart_config = False
|
||||||
if isLoad == True:
|
if op == "SET" and key not in self.ntp_servers:
|
||||||
if data != {}:
|
restart_config = True
|
||||||
self.has_ntp_servers = True
|
self.ntp_servers.add(key)
|
||||||
else:
|
elif op == "DEL" and key in self.ntp_servers:
|
||||||
# for runtime ntp server change, to determine if there is ntp server configured, need to
|
restart_config = True
|
||||||
# get from configDB, as delete triggers 2 event handling
|
self.ntp_servers.remove(key)
|
||||||
ntp_servers_tbl = self.config_db.get_table('NTP_SERVER')
|
|
||||||
if ntp_servers_tbl != {}:
|
|
||||||
self.has_ntp_servers = True
|
|
||||||
else:
|
|
||||||
self.has_ntp_servers = False
|
|
||||||
|
|
||||||
cmd = 'systemctl restart ntp-config'
|
|
||||||
syslog.syslog(syslog.LOG_INFO, 'ntp server update, restarting ntp-config, ntp server exists {}'.format(self.has_ntp_servers))
|
|
||||||
|
|
||||||
run_cmd(cmd)
|
|
||||||
|
|
||||||
|
if restart_config:
|
||||||
|
cmd = 'systemctl restart ntp-config'
|
||||||
|
syslog.syslog(syslog.LOG_INFO, 'ntp server update, restarting ntp-config, ntp servers configured {}'.format(self.ntp_servers))
|
||||||
|
run_cmd(cmd)
|
||||||
|
|
||||||
class HostConfigDaemon:
|
class HostConfigDaemon:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
# Just a sanity check to verify if the CONFIG_DB has been initialized
|
||||||
|
# before moving forward
|
||||||
self.config_db = ConfigDBConnector()
|
self.config_db = ConfigDBConnector()
|
||||||
self.config_db.connect(wait_for_init=True, retry_on=True)
|
self.config_db.connect(wait_for_init=True, retry_on=True)
|
||||||
|
self.dbconn = DBConnector(CFG_DB, 0)
|
||||||
|
self.selector = Select()
|
||||||
syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success')
|
syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success')
|
||||||
|
|
||||||
|
self.select = Select()
|
||||||
|
self.callbacks = dict()
|
||||||
|
self.subscriber_map = dict()
|
||||||
|
|
||||||
# Load DEVICE metadata configurations
|
# Load DEVICE metadata configurations
|
||||||
self.device_config = {}
|
self.device_config = {}
|
||||||
self.device_config['DEVICE_METADATA'] = self.config_db.get_table('DEVICE_METADATA')
|
self.device_config['DEVICE_METADATA'] = self.config_db.get_table('DEVICE_METADATA')
|
||||||
|
|
||||||
self.hostname_cache=""
|
# Initialize KDump Config and set the config to default if nothing is provided
|
||||||
self.aaacfg = AaaCfg()
|
|
||||||
self.iptables = Iptables()
|
|
||||||
self.feature_handler = FeatureHandler(self.config_db, self.device_config)
|
|
||||||
self.ntpcfg = NtpCfg(self.config_db)
|
|
||||||
|
|
||||||
self.is_multi_npu = device_info.is_multi_npu()
|
|
||||||
|
|
||||||
# Load Kdump configuration
|
|
||||||
self.kdumpCfg = KdumpCfg(self.config_db)
|
self.kdumpCfg = KdumpCfg(self.config_db)
|
||||||
self.kdumpCfg.load(self.config_db.get_table('KDUMP'))
|
self.kdumpCfg.load(self.config_db.get_table('KDUMP'))
|
||||||
|
|
||||||
|
# Initialize IpTables
|
||||||
|
self.iptables = Iptables()
|
||||||
|
|
||||||
|
# Intialize Feature Handler
|
||||||
|
self.feature_handler = FeatureHandler(self.config_db, self.device_config)
|
||||||
|
self.feature_handler.sync_state_field()
|
||||||
|
|
||||||
|
# Initialize Ntp Config Handler
|
||||||
|
self.ntpcfg = NtpCfg()
|
||||||
|
|
||||||
|
self.is_multi_npu = device_info.is_multi_npu()
|
||||||
|
|
||||||
|
# Initialize AAACfg
|
||||||
|
self.hostname_cache=""
|
||||||
|
self.aaacfg = AaaCfg()
|
||||||
|
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
aaa = self.config_db.get_table('AAA')
|
aaa = self.config_db.get_table('AAA')
|
||||||
@ -898,14 +931,6 @@ class HostConfigDaemon:
|
|||||||
radius_server = self.config_db.get_table('RADIUS_SERVER')
|
radius_server = self.config_db.get_table('RADIUS_SERVER')
|
||||||
self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server)
|
self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server)
|
||||||
|
|
||||||
lpbk_table = self.config_db.get_table('LOOPBACK_INTERFACE')
|
|
||||||
self.iptables.load(lpbk_table)
|
|
||||||
|
|
||||||
# Load NTP configurations
|
|
||||||
ntp_server = self.config_db.get_table('NTP_SERVER')
|
|
||||||
ntp_global = self.config_db.get_table('NTP')
|
|
||||||
self.ntpcfg.load(ntp_global, ntp_server)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dev_meta = self.config_db.get_table('DEVICE_METADATA')
|
dev_meta = self.config_db.get_table('DEVICE_METADATA')
|
||||||
if 'localhost' in dev_meta:
|
if 'localhost' in dev_meta:
|
||||||
@ -913,130 +938,171 @@ class HostConfigDaemon:
|
|||||||
self.hostname_cache = dev_meta['localhost']['hostname']
|
self.hostname_cache = dev_meta['localhost']['hostname']
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Update AAA with the hostname
|
# Update AAA with the hostname
|
||||||
self.aaacfg.hostname_update(self.hostname_cache)
|
self.aaacfg.hostname_update(self.hostname_cache)
|
||||||
|
|
||||||
def aaa_handler(self, key, data):
|
def __get_intf_name(self, key):
|
||||||
self.aaacfg.aaa_update(key, data)
|
if isinstance(key, tuple) and key:
|
||||||
|
intf = key[0]
|
||||||
|
else:
|
||||||
|
intf = key
|
||||||
|
return intf
|
||||||
|
|
||||||
def tacacs_server_handler(self, key, data):
|
def aaa_handler(self, key, op, data):
|
||||||
|
self.aaacfg.aaa_update(key, data)
|
||||||
|
syslog.syslog(syslog.LOG_INFO, 'AAA Update: key: {}, op: {}, data: {}'.format(key, op, data))
|
||||||
|
|
||||||
|
def tacacs_server_handler(self, key, op, data):
|
||||||
self.aaacfg.tacacs_server_update(key, data)
|
self.aaacfg.tacacs_server_update(key, data)
|
||||||
log_data = copy.deepcopy(data)
|
log_data = copy.deepcopy(data)
|
||||||
if 'passkey' in log_data:
|
if 'passkey' in log_data:
|
||||||
log_data['passkey'] = obfuscate(log_data['passkey'])
|
log_data['passkey'] = obfuscate(log_data['passkey'])
|
||||||
syslog.syslog(syslog.LOG_INFO, 'value of {} changed to {}'.format(key, log_data))
|
syslog.syslog(syslog.LOG_INFO, 'TACPLUS_SERVER update: key: {}, op: {}, data: {}'.format(key, op, log_data))
|
||||||
|
|
||||||
def tacacs_global_handler(self, key, data):
|
def tacacs_global_handler(self, key, op, data):
|
||||||
self.aaacfg.tacacs_global_update(key, data)
|
self.aaacfg.tacacs_global_update(key, data)
|
||||||
log_data = copy.deepcopy(data)
|
log_data = copy.deepcopy(data)
|
||||||
if 'passkey' in log_data:
|
if 'passkey' in log_data:
|
||||||
log_data['passkey'] = obfuscate(log_data['passkey'])
|
log_data['passkey'] = obfuscate(log_data['passkey'])
|
||||||
syslog.syslog(syslog.LOG_INFO, 'value of {} changed to {}'.format(key, log_data))
|
syslog.syslog(syslog.LOG_INFO, 'TACPLUS Global update: key: {}, op: {}, data: {}'.format(key, op, log_data))
|
||||||
|
|
||||||
def radius_server_handler(self, key, data):
|
def radius_server_handler(self, key, op, data):
|
||||||
self.aaacfg.radius_server_update(key, data)
|
self.aaacfg.radius_server_update(key, data)
|
||||||
log_data = copy.deepcopy(data)
|
log_data = copy.deepcopy(data)
|
||||||
if 'passkey' in log_data:
|
if 'passkey' in log_data:
|
||||||
log_data['passkey'] = obfuscate(log_data['passkey'])
|
log_data['passkey'] = obfuscate(log_data['passkey'])
|
||||||
syslog.syslog(syslog.LOG_INFO, 'value of {} changed to {}'.format(key, log_data))
|
syslog.syslog(syslog.LOG_INFO, 'RADIUS_SERVER update: key: {}, op: {}, data: {}'.format(key, op, log_data))
|
||||||
|
|
||||||
def radius_global_handler(self, key, data):
|
def radius_global_handler(self, key, op, data):
|
||||||
self.aaacfg.radius_global_update(key, data)
|
self.aaacfg.radius_global_update(key, data)
|
||||||
log_data = copy.deepcopy(data)
|
log_data = copy.deepcopy(data)
|
||||||
if 'passkey' in log_data:
|
if 'passkey' in log_data:
|
||||||
log_data['passkey'] = obfuscate(log_data['passkey'])
|
log_data['passkey'] = obfuscate(log_data['passkey'])
|
||||||
syslog.syslog(syslog.LOG_INFO, 'value of {} changed to {}'.format(key, log_data))
|
syslog.syslog(syslog.LOG_INFO, 'RADIUS Global update: key: {}, op: {}, data: {}'.format(key, op, log_data))
|
||||||
|
|
||||||
def mgmt_intf_handler(self, key, data):
|
def mgmt_intf_handler(self, key, op, data):
|
||||||
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
|
||||||
self.aaacfg.handle_radius_nas_ip_chg(key)
|
|
||||||
|
|
||||||
def lpbk_handler(self, key, data):
|
|
||||||
key = ConfigDBConnector.deserialize_key(key)
|
key = ConfigDBConnector.deserialize_key(key)
|
||||||
# Check if delete operation by fetch existing keys
|
mgmt_intf_name = self.__get_intf_name(key)
|
||||||
keys = self.config_db.get_keys('LOOPBACK_INTERFACE')
|
self.aaacfg.handle_radius_source_intf_ip_chg(mgmt_intf_name)
|
||||||
if key in keys:
|
self.aaacfg.handle_radius_nas_ip_chg(mgmt_intf_name)
|
||||||
add = True
|
|
||||||
else:
|
def lpbk_handler(self, key, op, data):
|
||||||
|
key = ConfigDBConnector.deserialize_key(key)
|
||||||
|
if op == "DEL":
|
||||||
add = False
|
add = False
|
||||||
|
else:
|
||||||
|
add = True
|
||||||
|
|
||||||
self.iptables.iptables_handler(key, data, add)
|
self.iptables.iptables_handler(key, data, add)
|
||||||
self.ntpcfg.handle_ntp_source_intf_chg(key)
|
lpbk_name = self.__get_intf_name(key)
|
||||||
|
self.ntpcfg.handle_ntp_source_intf_chg(lpbk_name)
|
||||||
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
||||||
|
|
||||||
def vlan_intf_handler(self, key, data):
|
def vlan_intf_handler(self, key, op, data):
|
||||||
key = ConfigDBConnector.deserialize_key(key)
|
key = ConfigDBConnector.deserialize_key(key)
|
||||||
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
||||||
|
|
||||||
def vlan_sub_intf_handler(self, key, data):
|
def vlan_sub_intf_handler(self, key, op, data):
|
||||||
key = ConfigDBConnector.deserialize_key(key)
|
key = ConfigDBConnector.deserialize_key(key)
|
||||||
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
||||||
|
|
||||||
def portchannel_intf_handler(self, key, data):
|
def portchannel_intf_handler(self, key, op, data):
|
||||||
key = ConfigDBConnector.deserialize_key(key)
|
key = ConfigDBConnector.deserialize_key(key)
|
||||||
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
||||||
|
|
||||||
def phy_intf_handler(self, key, data):
|
def phy_intf_handler(self, key, op, data):
|
||||||
key = ConfigDBConnector.deserialize_key(key)
|
key = ConfigDBConnector.deserialize_key(key)
|
||||||
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
self.aaacfg.handle_radius_source_intf_ip_chg(key)
|
||||||
|
|
||||||
def ntp_server_handler (self, key, data):
|
def ntp_server_handler(self, key, op, data):
|
||||||
syslog.syslog(syslog.LOG_INFO, 'NTP server handler...')
|
self.ntpcfg.ntp_server_update(key, op)
|
||||||
ntp_server_db = self.config_db.get_table('NTP_SERVER')
|
|
||||||
data = ntp_server_db
|
|
||||||
self.ntpcfg.ntp_server_update(key, data, False)
|
|
||||||
|
|
||||||
def ntp_global_handler (self, key, data):
|
def ntp_global_handler(self, key, op, data):
|
||||||
syslog.syslog(syslog.LOG_INFO, 'NTP global handler...')
|
self.ntpcfg.ntp_global_update(key, data)
|
||||||
self.ntpcfg.ntp_global_update(key, data, False)
|
|
||||||
|
|
||||||
def kdump_handler (self, key, data):
|
def kdump_handler (self, key, op, data):
|
||||||
syslog.syslog(syslog.LOG_INFO, 'Kdump handler...')
|
syslog.syslog(syslog.LOG_INFO, 'Kdump handler...')
|
||||||
self.kdumpCfg.kdump_update(key, data, False)
|
self.kdumpCfg.kdump_update(key, data)
|
||||||
|
|
||||||
def wait_till_system_init_done(self):
|
def wait_till_system_init_done(self):
|
||||||
|
|
||||||
# No need to print the output in the log file so using the "--quiet"
|
# No need to print the output in the log file so using the "--quiet"
|
||||||
# flag
|
# flag
|
||||||
systemctl_cmd = "sudo systemctl is-system-running --wait --quiet"
|
systemctl_cmd = "sudo systemctl is-system-running --wait --quiet"
|
||||||
subprocess.call(systemctl_cmd, shell=True)
|
subprocess.call(systemctl_cmd, shell=True)
|
||||||
|
|
||||||
def start(self):
|
def subscribe(self, table, callback, pri):
|
||||||
self.config_db.subscribe('AAA', lambda table, key, data: self.aaa_handler(key, data))
|
try:
|
||||||
self.config_db.subscribe('TACPLUS_SERVER', lambda table, key, data: self.tacacs_server_handler(key, data))
|
if table not in self.callbacks:
|
||||||
self.config_db.subscribe('TACPLUS', lambda table, key, data: self.tacacs_global_handler(key, data))
|
self.callbacks[table] = []
|
||||||
self.config_db.subscribe('RADIUS_SERVER', lambda table, key, data: self.radius_server_handler(key, data))
|
subscriber = SubscriberStateTable(self.dbconn, table, TableConsumable.DEFAULT_POP_BATCH_SIZE, pri)
|
||||||
self.config_db.subscribe('RADIUS', lambda table, key, data: self.radius_global_handler(key, data))
|
self.selector.addSelectable(subscriber) # Add to the Selector
|
||||||
self.config_db.subscribe('MGMT_INTERFACE', lambda table, key, data: self.mgmt_intf_handler(key, data))
|
self.subscriber_map[subscriber.getFd()] = (subscriber, table) # Maintain a mapping b/w subscriber & fd
|
||||||
self.config_db.subscribe('LOOPBACK_INTERFACE', lambda table, key, data: self.lpbk_handler(key, data))
|
|
||||||
self.config_db.subscribe('FEATURE', lambda table, key, data: self.feature_handler.handle(key, data))
|
|
||||||
self.config_db.subscribe('VLAN_INTERFACE', lambda table, key, data: self.vlan_intf_handler(key, data))
|
|
||||||
self.config_db.subscribe('VLAN_SUB_INTERFACE', lambda table, key, data: self.vlan_sub_intf_handler(key, data))
|
|
||||||
self.config_db.subscribe('PORTCHANNEL_INTERFACE', lambda table, key, data: self.portchannel_intf_handler(key, data))
|
|
||||||
self.config_db.subscribe('INTERFACE', lambda table, key, data: self.phy_intf_handler(key, data))
|
|
||||||
self.config_db.subscribe('NTP_SERVER', lambda table, key, data: self.ntp_server_handler(key, data))
|
|
||||||
self.config_db.subscribe('NTP', lambda table, key, data: self.ntp_global_handler(key, data))
|
|
||||||
self.config_db.subscribe('KDUMP', lambda table, key, data: self.kdump_handler(key, data))
|
|
||||||
|
|
||||||
|
self.callbacks[table].append(callback)
|
||||||
|
except Exception as err:
|
||||||
|
syslog.syslog(syslog.LOG_ERR, "Subscribe to table {} failed with error {}".format(table, err))
|
||||||
|
|
||||||
|
def register_callbacks(self):
|
||||||
|
self.subscribe('KDUMP', lambda table, key, op, data: self.kdump_handler(key, op, data), HOSTCFGD_MAX_PRI)
|
||||||
|
# Handle FEATURE updates before other tables
|
||||||
|
self.subscribe('FEATURE', lambda table, key, op, data: self.feature_handler.handle(key, op, data), HOSTCFGD_MAX_PRI-1)
|
||||||
|
# Handle AAA, TACACS and RADIUS related tables
|
||||||
|
self.subscribe('AAA', lambda table, key, op, data: self.aaa_handler(key, op, data), HOSTCFGD_MAX_PRI-2)
|
||||||
|
self.subscribe('TACPLUS', lambda table, key, op, data: self.tacacs_global_handler(key, op, data), HOSTCFGD_MAX_PRI-2)
|
||||||
|
self.subscribe('TACPLUS_SERVER', lambda table, key, op, data: self.tacacs_server_handler(key, op, data), HOSTCFGD_MAX_PRI-2)
|
||||||
|
self.subscribe('RADIUS', lambda table, key, op, data: self.radius_global_handler(key, op, data), HOSTCFGD_MAX_PRI-2)
|
||||||
|
self.subscribe('RADIUS_SERVER', lambda table, key, op, data: self.radius_server_handler(key, op, data), HOSTCFGD_MAX_PRI-2)
|
||||||
|
# Handle IPTables configuration
|
||||||
|
self.subscribe('LOOPBACK_INTERFACE', lambda table, key, op, data: self.lpbk_handler(key, op, data), HOSTCFGD_MAX_PRI-3)
|
||||||
|
# Handle NTP & NTP_SERVER updates
|
||||||
|
self.subscribe('NTP', lambda table, key, op, data: self.ntp_global_handler(key, op, data), HOSTCFGD_MAX_PRI-4)
|
||||||
|
self.subscribe('NTP_SERVER', lambda table, key, op, data: self.ntp_server_handler(key, op, data), HOSTCFGD_MAX_PRI-4)
|
||||||
|
# Handle updates to src intf changes in radius
|
||||||
|
self.subscribe('MGMT_INTERFACE', lambda table, key, op, data: self.mgmt_intf_handler(key, op, data), HOSTCFGD_MAX_PRI-5)
|
||||||
|
self.subscribe('VLAN_INTERFACE', lambda table, key, op, data: self.vlan_intf_handler(key, op, data), HOSTCFGD_MAX_PRI-5)
|
||||||
|
self.subscribe('VLAN_SUB_INTERFACE', lambda table, key, op, data: self.vlan_sub_intf_handler(key, op, data), HOSTCFGD_MAX_PRI-5)
|
||||||
|
self.subscribe('PORTCHANNEL_INTERFACE', lambda table, key, op, data: self.portchannel_intf_handler(key, op, data), HOSTCFGD_MAX_PRI-5)
|
||||||
|
self.subscribe('INTERFACE', lambda table, key, op, data: self.phy_intf_handler(key, op, data), HOSTCFGD_MAX_PRI-5)
|
||||||
|
|
||||||
syslog.syslog(syslog.LOG_INFO,
|
syslog.syslog(syslog.LOG_INFO,
|
||||||
"Waiting for systemctl to finish initialization")
|
"Waiting for systemctl to finish initialization")
|
||||||
self.wait_till_system_init_done()
|
self.wait_till_system_init_done()
|
||||||
syslog.syslog(syslog.LOG_INFO,
|
syslog.syslog(syslog.LOG_INFO,
|
||||||
"systemctl has finished initialization -- proceeding ...")
|
"systemctl has finished initialization -- proceeding ...")
|
||||||
|
|
||||||
# Update all feature states once upon starting
|
def start(self):
|
||||||
self.feature_handler.update_all_features_config()
|
while True:
|
||||||
|
state, selectable_ = self.selector.select(DEFAULT_SELECT_TIMEOUT)
|
||||||
|
if state == self.selector.TIMEOUT:
|
||||||
|
continue
|
||||||
|
elif state == self.selector.ERROR:
|
||||||
|
syslog.syslog(syslog.LOG_ERR,
|
||||||
|
"error returned by select")
|
||||||
|
continue
|
||||||
|
|
||||||
# Defer load until subscribe
|
fd = selectable_.getFd()
|
||||||
self.load()
|
# Get the Corresponding subscriber & table
|
||||||
|
subscriber, table = self.subscriber_map.get(fd, (None, ""))
|
||||||
self.config_db.listen()
|
if not subscriber:
|
||||||
|
syslog.syslog(syslog.LOG_ERR,
|
||||||
|
"No Subscriber object found for fd: {}, subscriber map: {}".format(fd, subscriber_map))
|
||||||
|
continue
|
||||||
|
key, op, fvs = subscriber.pop()
|
||||||
|
# Get the registered callback
|
||||||
|
cbs = self.callbacks.get(table, None)
|
||||||
|
for callback in cbs:
|
||||||
|
callback(table, key, op, dict(fvs))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGHUP, signal_handler)
|
||||||
daemon = HostConfigDaemon()
|
daemon = HostConfigDaemon()
|
||||||
|
daemon.register_callbacks()
|
||||||
|
daemon.load()
|
||||||
daemon.start()
|
daemon.start()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
@ -26,8 +26,7 @@ setup(
|
|||||||
'dbus-python',
|
'dbus-python',
|
||||||
'Jinja2>=2.10',
|
'Jinja2>=2.10',
|
||||||
'PyGObject',
|
'PyGObject',
|
||||||
'sonic-py-common',
|
'sonic-py-common'
|
||||||
'systemd-python',
|
|
||||||
],
|
],
|
||||||
setup_requires = [
|
setup_requires = [
|
||||||
'pytest-runner',
|
'pytest-runner',
|
||||||
@ -37,7 +36,8 @@ setup(
|
|||||||
'parameterized',
|
'parameterized',
|
||||||
'pytest',
|
'pytest',
|
||||||
'pyfakefs',
|
'pyfakefs',
|
||||||
'sonic-py-common'
|
'sonic-py-common',
|
||||||
|
'deepdiff'
|
||||||
],
|
],
|
||||||
classifiers = [
|
classifiers = [
|
||||||
'Development Status :: 3 - Alpha',
|
'Development Status :: 3 - Alpha',
|
||||||
|
118
src/sonic-host-services/tests/common/mock_configdb.py
Normal file
118
src/sonic-host-services/tests/common/mock_configdb.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
class MockConfigDb(object):
|
||||||
|
"""
|
||||||
|
Mock Config DB which responds to data tables requests and store updates to the data table
|
||||||
|
"""
|
||||||
|
STATE_DB = None
|
||||||
|
CONFIG_DB = None
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_config_db(test_config_db):
|
||||||
|
MockConfigDb.CONFIG_DB = test_config_db
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def deserialize_key(key, separator="|"):
|
||||||
|
tokens = key.split(separator)
|
||||||
|
if len(tokens) > 1:
|
||||||
|
return tuple(tokens)
|
||||||
|
else:
|
||||||
|
return key
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_config_db():
|
||||||
|
return MockConfigDb.CONFIG_DB
|
||||||
|
|
||||||
|
def connect(self, wait_for_init=True, retry_on=True):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get(self, db_id, key, field):
|
||||||
|
return MockConfigDb.CONFIG_DB[key][field]
|
||||||
|
|
||||||
|
def get_entry(self, key, field):
|
||||||
|
return MockConfigDb.CONFIG_DB[key][field]
|
||||||
|
|
||||||
|
def mod_entry(self, key, field, data):
|
||||||
|
existing_data = self.get_entry(key, field)
|
||||||
|
existing_data.update(data)
|
||||||
|
self.set_entry(key, field, existing_data)
|
||||||
|
|
||||||
|
def set_entry(self, key, field, data):
|
||||||
|
MockConfigDb.CONFIG_DB[key][field] = data
|
||||||
|
|
||||||
|
def get_table(self, table_name):
|
||||||
|
return MockConfigDb.CONFIG_DB[table_name]
|
||||||
|
|
||||||
|
|
||||||
|
class MockSelect():
|
||||||
|
|
||||||
|
event_queue = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_event_queue(Q):
|
||||||
|
MockSelect.event_queue = Q
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_event_queue():
|
||||||
|
return MockSelect.event_queue
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reset_event_queue():
|
||||||
|
MockSelect.event_queue = []
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.sub_map = {}
|
||||||
|
self.TIMEOUT = "TIMEOUT"
|
||||||
|
self.ERROR = "ERROR"
|
||||||
|
|
||||||
|
def addSelectable(self, subscriber):
|
||||||
|
self.sub_map[subscriber.table] = subscriber
|
||||||
|
|
||||||
|
def select(self, TIMEOUT):
|
||||||
|
if not MockSelect.get_event_queue():
|
||||||
|
raise TimeoutError
|
||||||
|
table, key = MockSelect.get_event_queue().pop(0)
|
||||||
|
self.sub_map[table].nextKey(key)
|
||||||
|
return "OBJECT", self.sub_map[table]
|
||||||
|
|
||||||
|
|
||||||
|
class MockSubscriberStateTable():
|
||||||
|
|
||||||
|
FD_INIT = 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_fd():
|
||||||
|
curr = MockSubscriberStateTable.FD_INIT
|
||||||
|
MockSubscriberStateTable.FD_INIT = curr + 1
|
||||||
|
return curr
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reset_fd():
|
||||||
|
MockSubscriberStateTable.FD_INIT = 0
|
||||||
|
|
||||||
|
def __init__(self, conn, table, pop, pri):
|
||||||
|
self.fd = MockSubscriberStateTable.generate_fd()
|
||||||
|
self.next_key = ''
|
||||||
|
self.table = table
|
||||||
|
|
||||||
|
def getFd(self):
|
||||||
|
return self.fd
|
||||||
|
|
||||||
|
def nextKey(self, key):
|
||||||
|
self.next_key = key
|
||||||
|
|
||||||
|
def pop(self):
|
||||||
|
table = MockConfigDb.CONFIG_DB.get(self.table, {})
|
||||||
|
if self.next_key not in table:
|
||||||
|
op = "DEL"
|
||||||
|
fvs = {}
|
||||||
|
else:
|
||||||
|
op = "SET"
|
||||||
|
fvs = table.get(self.next_key, {})
|
||||||
|
return self.next_key, op, fvs
|
||||||
|
|
||||||
|
|
||||||
|
class MockDBConnector():
|
||||||
|
def __init__(self, db, val):
|
||||||
|
pass
|
@ -10,10 +10,10 @@ from swsscommon import swsscommon
|
|||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
from unittest import TestCase, mock
|
from unittest import TestCase, mock
|
||||||
from tests.hostcfgd.test_radius_vectors import HOSTCFGD_TEST_RADIUS_VECTOR
|
from tests.hostcfgd.test_radius_vectors import HOSTCFGD_TEST_RADIUS_VECTOR
|
||||||
from tests.hostcfgd.mock_configdb import MockConfigDb
|
from tests.common.mock_configdb import MockConfigDb, MockSubscriberStateTable
|
||||||
|
from tests.common.mock_configdb import MockSelect, MockDBConnector
|
||||||
|
|
||||||
|
|
||||||
swsscommon.ConfigDBConnector = MockConfigDb
|
|
||||||
test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
modules_path = os.path.dirname(test_path)
|
modules_path = os.path.dirname(test_path)
|
||||||
scripts_path = os.path.join(modules_path, "scripts")
|
scripts_path = os.path.join(modules_path, "scripts")
|
||||||
@ -31,6 +31,12 @@ hostcfgd = importlib.util.module_from_spec(spec)
|
|||||||
loader.exec_module(hostcfgd)
|
loader.exec_module(hostcfgd)
|
||||||
sys.modules['hostcfgd'] = hostcfgd
|
sys.modules['hostcfgd'] = hostcfgd
|
||||||
|
|
||||||
|
# Mock swsscommon classes
|
||||||
|
hostcfgd.ConfigDBConnector = MockConfigDb
|
||||||
|
hostcfgd.SubscriberStateTable = MockSubscriberStateTable
|
||||||
|
hostcfgd.Select = MockSelect
|
||||||
|
hostcfgd.DBConnector = MockDBConnector
|
||||||
|
|
||||||
|
|
||||||
class TestHostcfgdRADIUS(TestCase):
|
class TestHostcfgdRADIUS(TestCase):
|
||||||
"""
|
"""
|
||||||
@ -44,11 +50,9 @@ class TestHostcfgdRADIUS(TestCase):
|
|||||||
def test_hostcfgd_radius(self, test_name, test_data):
|
def test_hostcfgd_radius(self, test_name, test_data):
|
||||||
"""
|
"""
|
||||||
Test RADIUS hostcfd daemon initialization
|
Test RADIUS hostcfd daemon initialization
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
test_name(str): test name
|
test_name(str): test name
|
||||||
test_data(dict): test data which contains initial Config Db tables, and expected results
|
test_data(dict): test data which contains initial Config Db tables, and expected results
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
|
@ -1,26 +1,32 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import swsscommon
|
import swsscommon as swsscommon_package
|
||||||
|
from swsscommon import swsscommon
|
||||||
|
|
||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
from sonic_py_common.general import load_module_from_source
|
from sonic_py_common.general import load_module_from_source
|
||||||
from unittest import TestCase, mock
|
from unittest import TestCase, mock
|
||||||
|
|
||||||
from .test_vectors import HOSTCFGD_TEST_VECTOR
|
from .test_vectors import HOSTCFGD_TEST_VECTOR, HOSTCFG_DAEMON_CFG_DB
|
||||||
from .mock_configdb import MockConfigDb
|
from tests.common.mock_configdb import MockConfigDb, MockSubscriberStateTable
|
||||||
|
from tests.common.mock_configdb import MockSelect, MockDBConnector
|
||||||
|
|
||||||
from pyfakefs.fake_filesystem_unittest import patchfs
|
from pyfakefs.fake_filesystem_unittest import patchfs
|
||||||
|
from deepdiff import DeepDiff
|
||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
|
||||||
swsscommon.swsscommon.ConfigDBConnector = MockConfigDb
|
|
||||||
test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
test_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
modules_path = os.path.dirname(test_path)
|
modules_path = os.path.dirname(test_path)
|
||||||
scripts_path = os.path.join(modules_path, "scripts")
|
scripts_path = os.path.join(modules_path, 'scripts')
|
||||||
sys.path.insert(0, modules_path)
|
sys.path.insert(0, modules_path)
|
||||||
|
|
||||||
# Load the file under test
|
# Load the file under test
|
||||||
hostcfgd_path = os.path.join(scripts_path, 'hostcfgd')
|
hostcfgd_path = os.path.join(scripts_path, 'hostcfgd')
|
||||||
hostcfgd = load_module_from_source('hostcfgd', hostcfgd_path)
|
hostcfgd = load_module_from_source('hostcfgd', hostcfgd_path)
|
||||||
|
hostcfgd.ConfigDBConnector = MockConfigDb
|
||||||
|
hostcfgd.SubscriberStateTable = MockSubscriberStateTable
|
||||||
|
hostcfgd.Select = MockSelect
|
||||||
|
hostcfgd.DBConnector = MockDBConnector
|
||||||
|
|
||||||
|
|
||||||
class TestHostcfgd(TestCase):
|
class TestHostcfgd(TestCase):
|
||||||
@ -40,18 +46,36 @@ class TestHostcfgd(TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
is_equal = len(table) == len(expected_table)
|
ddiff = DeepDiff(table, expected_table, ignore_order=True)
|
||||||
if is_equal:
|
print('DIFF:', ddiff)
|
||||||
for key, fields in expected_table.items():
|
return True if not ddiff else False
|
||||||
is_equal = is_equal and key in table and len(fields) == len(table[key])
|
|
||||||
if is_equal:
|
def __verify_fs(self, table):
|
||||||
for field, value in fields.items():
|
"""
|
||||||
is_equal = is_equal and value == table[key][field]
|
verify filesystem changes made by hostcfgd.
|
||||||
if not is_equal:
|
|
||||||
break;
|
Checks whether systemd override configuration files
|
||||||
else:
|
were generated and Restart= for systemd unit is set
|
||||||
break
|
correctly
|
||||||
return is_equal
|
|
||||||
|
Args:
|
||||||
|
table(dict): Current Config Db table
|
||||||
|
|
||||||
|
Returns: Boolean wether test passed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
exp_dict = {
|
||||||
|
'enabled': 'always',
|
||||||
|
'disabled': 'no',
|
||||||
|
}
|
||||||
|
auto_restart_conf = os.path.join(hostcfgd.FeatureHandler.SYSTEMD_SERVICE_CONF_DIR, 'auto_restart.conf')
|
||||||
|
|
||||||
|
for feature in table:
|
||||||
|
auto_restart = table[feature].get('auto_restart', 'disabled')
|
||||||
|
with open(auto_restart_conf.format(feature)) as conf:
|
||||||
|
conf = conf.read().strip()
|
||||||
|
assert conf == '[Service]\nRestart={}'.format(exp_dict[auto_restart])
|
||||||
|
|
||||||
|
|
||||||
def __verify_fs(self, table):
|
def __verify_fs(self, table):
|
||||||
"""
|
"""
|
||||||
@ -82,9 +106,9 @@ class TestHostcfgd(TestCase):
|
|||||||
|
|
||||||
@parameterized.expand(HOSTCFGD_TEST_VECTOR)
|
@parameterized.expand(HOSTCFGD_TEST_VECTOR)
|
||||||
@patchfs
|
@patchfs
|
||||||
def test_hostcfgd(self, test_name, test_data, fs):
|
def test_hostcfgd_feature_handler(self, test_name, test_data, fs):
|
||||||
"""
|
"""
|
||||||
Test hostcfd daemon initialization
|
Test feature config capability in the hostcfd
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
test_name(str): test name
|
test_name(str): test name
|
||||||
@ -93,24 +117,34 @@ class TestHostcfgd(TestCase):
|
|||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
fs.add_real_paths(swsscommon.__path__) # add real path of swsscommon for database_config.json
|
fs.add_real_paths(swsscommon_package.__path__) # add real path of swsscommon for database_config.json
|
||||||
fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR)
|
fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR)
|
||||||
MockConfigDb.set_config_db(test_data["config_db"])
|
MockConfigDb.set_config_db(test_data['config_db'])
|
||||||
with mock.patch("hostcfgd.subprocess") as mocked_subprocess:
|
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
|
||||||
popen_mock = mock.Mock()
|
popen_mock = mock.Mock()
|
||||||
attrs = test_data["popen_attributes"]
|
attrs = test_data['popen_attributes']
|
||||||
popen_mock.configure_mock(**attrs)
|
popen_mock.configure_mock(**attrs)
|
||||||
mocked_subprocess.Popen.return_value = popen_mock
|
mocked_subprocess.Popen.return_value = popen_mock
|
||||||
|
|
||||||
host_config_daemon = hostcfgd.HostConfigDaemon()
|
# Initialize Feature Handler
|
||||||
host_config_daemon.feature_handler.update_all_features_config()
|
device_config = {}
|
||||||
assert self.__verify_table(
|
device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA']
|
||||||
MockConfigDb.get_config_db()["FEATURE"],
|
feature_handler = hostcfgd.FeatureHandler(MockConfigDb(), device_config)
|
||||||
test_data["expected_config_db"]["FEATURE"]
|
|
||||||
), "Test failed for test data: {0}".format(test_data)
|
|
||||||
mocked_subprocess.check_call.assert_has_calls(test_data["expected_subprocess_calls"], any_order=True)
|
|
||||||
|
|
||||||
self.__verify_fs(test_data["config_db"]["FEATURE"])
|
# sync the state field and Handle Feature Updates
|
||||||
|
feature_handler.sync_state_field()
|
||||||
|
features = MockConfigDb.CONFIG_DB['FEATURE']
|
||||||
|
for key, fvs in features.items():
|
||||||
|
feature_handler.handle(key, 'SET', fvs)
|
||||||
|
|
||||||
|
# Verify if the updates are properly updated
|
||||||
|
assert self.__verify_table(
|
||||||
|
MockConfigDb.get_config_db()['FEATURE'],
|
||||||
|
test_data['expected_config_db']['FEATURE']
|
||||||
|
), 'Test failed for test data: {0}'.format(test_data)
|
||||||
|
mocked_subprocess.check_call.assert_has_calls(test_data['expected_subprocess_calls'], any_order=True)
|
||||||
|
|
||||||
|
self.__verify_fs(test_data['config_db']['FEATURE'])
|
||||||
|
|
||||||
def test_feature_config_parsing(self):
|
def test_feature_config_parsing(self):
|
||||||
swss_feature = hostcfgd.Feature('swss', {
|
swss_feature = hostcfgd.Feature('swss', {
|
||||||
@ -139,3 +173,150 @@ class TestHostcfgd(TestCase):
|
|||||||
assert not swss_feature.has_timer
|
assert not swss_feature.has_timer
|
||||||
assert swss_feature.has_global_scope
|
assert swss_feature.has_global_scope
|
||||||
assert not swss_feature.has_per_asic_scope
|
assert not swss_feature.has_per_asic_scope
|
||||||
|
|
||||||
|
|
||||||
|
class TesNtpCfgd(TestCase):
|
||||||
|
"""
|
||||||
|
Test hostcfd daemon - NtpCfgd
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
MockConfigDb.CONFIG_DB['NTP'] = {'global': {'vrf': 'mgmt', 'src_intf': 'eth0'}}
|
||||||
|
MockConfigDb.CONFIG_DB['NTP_SERVER'] = {'0.debian.pool.ntp.org': {}}
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
MockConfigDb.CONFIG_DB = {}
|
||||||
|
|
||||||
|
def test_ntp_global_update_with_no_servers(self):
|
||||||
|
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
|
||||||
|
popen_mock = mock.Mock()
|
||||||
|
attrs = {'communicate.return_value': ('output', 'error')}
|
||||||
|
popen_mock.configure_mock(**attrs)
|
||||||
|
mocked_subprocess.Popen.return_value = popen_mock
|
||||||
|
|
||||||
|
ntpcfgd = hostcfgd.NtpCfg()
|
||||||
|
ntpcfgd.ntp_global_update('global', MockConfigDb.CONFIG_DB['NTP']['global'])
|
||||||
|
|
||||||
|
mocked_subprocess.check_call.assert_not_called()
|
||||||
|
|
||||||
|
def test_ntp_global_update_ntp_servers(self):
|
||||||
|
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
|
||||||
|
popen_mock = mock.Mock()
|
||||||
|
attrs = {'communicate.return_value': ('output', 'error')}
|
||||||
|
popen_mock.configure_mock(**attrs)
|
||||||
|
mocked_subprocess.Popen.return_value = popen_mock
|
||||||
|
|
||||||
|
ntpcfgd = hostcfgd.NtpCfg()
|
||||||
|
ntpcfgd.ntp_global_update('global', MockConfigDb.CONFIG_DB['NTP']['global'])
|
||||||
|
ntpcfgd.ntp_server_update('0.debian.pool.ntp.org', 'SET')
|
||||||
|
mocked_subprocess.check_call.assert_has_calls([call('systemctl restart ntp-config', shell=True)])
|
||||||
|
|
||||||
|
def test_loopback_update(self):
|
||||||
|
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
|
||||||
|
popen_mock = mock.Mock()
|
||||||
|
attrs = {'communicate.return_value': ('output', 'error')}
|
||||||
|
popen_mock.configure_mock(**attrs)
|
||||||
|
mocked_subprocess.Popen.return_value = popen_mock
|
||||||
|
|
||||||
|
ntpcfgd = hostcfgd.NtpCfg()
|
||||||
|
ntpcfgd.ntp_global = MockConfigDb.CONFIG_DB['NTP']['global']
|
||||||
|
ntpcfgd.ntp_servers.add('0.debian.pool.ntp.org')
|
||||||
|
|
||||||
|
ntpcfgd.handle_ntp_source_intf_chg('eth0')
|
||||||
|
mocked_subprocess.check_call.assert_has_calls([call('systemctl restart ntp-config', shell=True)])
|
||||||
|
|
||||||
|
|
||||||
|
class TestHostcfgdDaemon(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
MockConfigDb.set_config_db(HOSTCFG_DAEMON_CFG_DB)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
MockConfigDb.CONFIG_DB = {}
|
||||||
|
|
||||||
|
@patchfs
|
||||||
|
def test_feature_events(self, fs):
|
||||||
|
fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR)
|
||||||
|
MockSelect.event_queue = [('FEATURE', 'dhcp_relay'),
|
||||||
|
('FEATURE', 'mux'),
|
||||||
|
('FEATURE', 'telemetry')]
|
||||||
|
daemon = hostcfgd.HostConfigDaemon()
|
||||||
|
daemon.register_callbacks()
|
||||||
|
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
|
||||||
|
popen_mock = mock.Mock()
|
||||||
|
attrs = {'communicate.return_value': ('output', 'error')}
|
||||||
|
popen_mock.configure_mock(**attrs)
|
||||||
|
mocked_subprocess.Popen.return_value = popen_mock
|
||||||
|
try:
|
||||||
|
daemon.start()
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
expected = [call('sudo systemctl daemon-reload', shell=True),
|
||||||
|
call('sudo systemctl unmask dhcp_relay.service', shell=True),
|
||||||
|
call('sudo systemctl enable dhcp_relay.service', shell=True),
|
||||||
|
call('sudo systemctl start dhcp_relay.service', shell=True),
|
||||||
|
call('sudo systemctl daemon-reload', shell=True),
|
||||||
|
call('sudo systemctl unmask mux.service', shell=True),
|
||||||
|
call('sudo systemctl enable mux.service', shell=True),
|
||||||
|
call('sudo systemctl start mux.service', shell=True),
|
||||||
|
call('sudo systemctl daemon-reload', shell=True),
|
||||||
|
call('sudo systemctl unmask telemetry.service', shell=True),
|
||||||
|
call('sudo systemctl unmask telemetry.timer', shell=True),
|
||||||
|
call('sudo systemctl enable telemetry.timer', shell=True),
|
||||||
|
call('sudo systemctl start telemetry.timer', shell=True)]
|
||||||
|
mocked_subprocess.check_call.assert_has_calls(expected)
|
||||||
|
|
||||||
|
# Change the state to disabled
|
||||||
|
MockConfigDb.CONFIG_DB['FEATURE']['telemetry']['state'] = 'disabled'
|
||||||
|
MockSelect.event_queue = [('FEATURE', 'telemetry')]
|
||||||
|
try:
|
||||||
|
daemon.start()
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
expected = [call('sudo systemctl stop telemetry.timer', shell=True),
|
||||||
|
call('sudo systemctl disable telemetry.timer', shell=True),
|
||||||
|
call('sudo systemctl mask telemetry.timer', shell=True),
|
||||||
|
call('sudo systemctl stop telemetry.service', shell=True),
|
||||||
|
call('sudo systemctl disable telemetry.timer', shell=True),
|
||||||
|
call('sudo systemctl mask telemetry.timer', shell=True)]
|
||||||
|
mocked_subprocess.check_call.assert_has_calls(expected)
|
||||||
|
|
||||||
|
def test_loopback_events(self):
|
||||||
|
MockConfigDb.set_config_db(HOSTCFG_DAEMON_CFG_DB)
|
||||||
|
MockSelect.event_queue = [('NTP', 'global'),
|
||||||
|
('NTP_SERVER', '0.debian.pool.ntp.org'),
|
||||||
|
('LOOPBACK_INTERFACE', 'Loopback0|10.184.8.233/32')]
|
||||||
|
daemon = hostcfgd.HostConfigDaemon()
|
||||||
|
daemon.register_callbacks()
|
||||||
|
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
|
||||||
|
popen_mock = mock.Mock()
|
||||||
|
attrs = {'communicate.return_value': ('output', 'error')}
|
||||||
|
popen_mock.configure_mock(**attrs)
|
||||||
|
mocked_subprocess.Popen.return_value = popen_mock
|
||||||
|
try:
|
||||||
|
daemon.start()
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
expected = [call('systemctl restart ntp-config', shell=True),
|
||||||
|
call('iptables -t mangle --append PREROUTING -p tcp --tcp-flags SYN SYN -d 10.184.8.233 -j TCPMSS --set-mss 1460', shell=True),
|
||||||
|
call('iptables -t mangle --append POSTROUTING -p tcp --tcp-flags SYN SYN -s 10.184.8.233 -j TCPMSS --set-mss 1460', shell=True)]
|
||||||
|
mocked_subprocess.check_call.assert_has_calls(expected, any_order=True)
|
||||||
|
|
||||||
|
def test_kdump_event(self):
|
||||||
|
MockConfigDb.set_config_db(HOSTCFG_DAEMON_CFG_DB)
|
||||||
|
daemon = hostcfgd.HostConfigDaemon()
|
||||||
|
daemon.register_callbacks()
|
||||||
|
assert MockConfigDb.CONFIG_DB['KDUMP']['config']
|
||||||
|
MockSelect.event_queue = [('KDUMP', 'config')]
|
||||||
|
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
|
||||||
|
popen_mock = mock.Mock()
|
||||||
|
attrs = {'communicate.return_value': ('output', 'error')}
|
||||||
|
popen_mock.configure_mock(**attrs)
|
||||||
|
mocked_subprocess.Popen.return_value = popen_mock
|
||||||
|
try:
|
||||||
|
daemon.start()
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
expected = [call('sonic-kdump-config --disable', shell=True),
|
||||||
|
call('sonic-kdump-config --num_dumps 3', shell=True),
|
||||||
|
call('sonic-kdump-config --memory 0M-2G:256M,2G-4G:320M,4G-8G:384M,8G-:448M', shell=True)]
|
||||||
|
mocked_subprocess.check_call.assert_has_calls(expected, any_order=True)
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
class MockConfigDb(object):
|
|
||||||
"""
|
|
||||||
Mock Config DB which responds to data tables requests and store updates to the data table
|
|
||||||
"""
|
|
||||||
STATE_DB = None
|
|
||||||
CONFIG_DB = None
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_config_db(test_config_db):
|
|
||||||
MockConfigDb.CONFIG_DB = test_config_db
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_config_db():
|
|
||||||
return MockConfigDb.CONFIG_DB
|
|
||||||
|
|
||||||
def connect(self, wait_for_init=True, retry_on=True):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get(self, db_id, key, field):
|
|
||||||
return MockConfigDb.CONFIG_DB[key][field]
|
|
||||||
|
|
||||||
def get_entry(self, key, field):
|
|
||||||
return MockConfigDb.CONFIG_DB[key][field]
|
|
||||||
|
|
||||||
def mod_entry(self, key, field, data):
|
|
||||||
existing_data = self.get_entry(key, field)
|
|
||||||
existing_data.update(data)
|
|
||||||
self.set_entry(key, field, existing_data)
|
|
||||||
|
|
||||||
def set_entry(self, key, field, data):
|
|
||||||
MockConfigDb.CONFIG_DB[key][field] = data
|
|
||||||
|
|
||||||
def get_table(self, table_name):
|
|
||||||
return MockConfigDb.CONFIG_DB[table_name]
|
|
@ -492,3 +492,62 @@ HOSTCFGD_TEST_VECTOR = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
HOSTCFG_DAEMON_CFG_DB = {
|
||||||
|
"FEATURE": {
|
||||||
|
"dhcp_relay": {
|
||||||
|
"auto_restart": "enabled",
|
||||||
|
"has_global_scope": "True",
|
||||||
|
"has_per_asic_scope": "False",
|
||||||
|
"has_timer": "False",
|
||||||
|
"high_mem_alert": "disabled",
|
||||||
|
"set_owner": "kube",
|
||||||
|
"state": "{% if not (DEVICE_METADATA is defined and DEVICE_METADATA['localhost'] is defined and DEVICE_METADATA['localhost']['type'] is defined and DEVICE_METADATA['localhost']['type'] != 'ToRRouter') %}enabled{% else %}disabled{% endif %}"
|
||||||
|
},
|
||||||
|
"mux": {
|
||||||
|
"auto_restart": "enabled",
|
||||||
|
"has_global_scope": "True",
|
||||||
|
"has_per_asic_scope": "False",
|
||||||
|
"has_timer": "False",
|
||||||
|
"high_mem_alert": "disabled",
|
||||||
|
"set_owner": "local",
|
||||||
|
"state": "{% if 'subtype' in DEVICE_METADATA['localhost'] and DEVICE_METADATA['localhost']['subtype'] == 'DualToR' %}enabled{% else %}always_disabled{% endif %}"
|
||||||
|
},
|
||||||
|
"telemetry": {
|
||||||
|
"auto_restart": "enabled",
|
||||||
|
"has_global_scope": "True",
|
||||||
|
"has_per_asic_scope": "False",
|
||||||
|
"has_timer": "True",
|
||||||
|
"high_mem_alert": "disabled",
|
||||||
|
"set_owner": "kube",
|
||||||
|
"state": "enabled",
|
||||||
|
"status": "enabled"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"KDUMP": {
|
||||||
|
"config": {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"NTP": {
|
||||||
|
"global": {
|
||||||
|
"vrf": "default",
|
||||||
|
"src_intf": "eth0;Loopback0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"NTP_SERVER": {
|
||||||
|
"0.debian.pool.ntp.org": {}
|
||||||
|
},
|
||||||
|
"LOOPBACK_INTERFACE": {
|
||||||
|
"Loopback0|10.184.8.233/32": {
|
||||||
|
"scope": "global",
|
||||||
|
"family": "IPv4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DEVICE_METADATA": {
|
||||||
|
"localhost": {
|
||||||
|
"subtype": "DualToR",
|
||||||
|
"type": "ToRRouter",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user