[hostcfgd] record feature state in STATE DB (#9842)

- Why I did it
To implement blocking feature state change.

- How I did it
Record the actual feature state in STATE DB from hostcfg.

- How to verify it
UT + verification by running on the switch and checking STATE DB.

Signed-off-by: Stepan Blyschak <stepanb@nvidia.com>
This commit is contained in:
Stepan Blyshchak 2022-03-14 13:45:27 +02:00 committed by GitHub
parent 3fa627f290
commit 2919b4820f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 59 additions and 21 deletions

View File

@ -82,7 +82,7 @@ def get_current_running_from_DB(always_running_containers):
state_db = swsscommon.DBConnector("STATE_DB", 0)
tbl = swsscommon.Table(state_db, "FEATURE")
if not tbl.getKeys():
return False, None
return running_containers
for name in tbl.getKeys():
data = dict(tbl.get(name)[1])
@ -101,7 +101,7 @@ def get_current_running_from_DB(always_running_containers):
print("Failed to get container '{}'. Error: '{}'".format(name, err))
pass
return True, running_containers
return running_containers
def get_current_running_from_dockers():
@ -132,9 +132,8 @@ def get_current_running_containers(always_running_containers):
@return: A set of currently running containers.
"""
ret, current_running_containers = get_current_running_from_DB(always_running_containers)
if not ret:
current_running_containers = get_current_running_from_dockers()
current_running_containers = get_current_running_from_DB(always_running_containers)
current_running_containers.update(get_current_running_from_dockers())
return current_running_containers

View File

@ -12,7 +12,7 @@ import signal
import jinja2
from sonic_py_common import device_info
from swsscommon.swsscommon import SubscriberStateTable, DBConnector, Select
from swsscommon.swsscommon import ConfigDBConnector, TableConsumable
from swsscommon.swsscommon import ConfigDBConnector, TableConsumable, Table
# FILE
PAM_AUTH_CONF = "/etc/pam.d/common-auth-sonic"
@ -41,6 +41,7 @@ RADIUS_PAM_AUTH_CONF_DIR = "/etc/pam_radius_auth.d/"
# MISC Constants
CFG_DB = "CONFIG_DB"
STATE_DB = "STATE_DB"
HOSTCFGD_MAX_PRI = 10 # Used to enforce ordering b/w daemons under Hostcfgd
DEFAULT_SELECT_TIMEOUT = 1000
@ -166,16 +167,23 @@ class FeatureHandler(object):
SYSTEMD_SYSTEM_DIR = '/etc/systemd/system/'
SYSTEMD_SERVICE_CONF_DIR = os.path.join(SYSTEMD_SYSTEM_DIR, '{}.service.d/')
def __init__(self, config_db, device_config):
# Feature state constants
FEATURE_STATE_ENABLED = "enabled"
FEATURE_STATE_DISABLED = "disabled"
FEATURE_STATE_FAILED = "failed"
def __init__(self, config_db, feature_state_table, device_config):
self._config_db = config_db
self._feature_state_table = feature_state_table
self._device_config = device_config
self._cached_config = {}
self.is_multi_npu = device_info.is_multi_npu()
def handle(self, feature_name, op, feature_cfg):
if not feature_cfg:
self._cached_config.pop(feature_name)
syslog.syslog(syslog.LOG_INFO, "Deregistering feature {}".format(feature_name))
self._cached_config.pop(feature_name)
self._feature_state_table._del(feature_name)
return
feature = Feature(feature_name, feature_cfg, self._device_config)
@ -253,7 +261,6 @@ class FeatureHandler(object):
return True
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')
@ -341,8 +348,11 @@ class FeatureHandler(object):
except Exception as err:
syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be enabled and started"
.format(feature.name, feature_suffixes[-1]))
self.set_feature_state(feature, self.FEATURE_STATE_FAILED)
return
self.set_feature_state(feature, self.FEATURE_STATE_ENABLED)
def disable_feature(self, feature):
cmds = []
feature_names, feature_suffixes = self.get_feature_attribute(feature)
@ -363,11 +373,17 @@ class FeatureHandler(object):
except Exception as err:
syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be stopped and disabled"
.format(feature.name, feature_suffixes[-1]))
self.set_feature_state(feature, self.FEATURE_STATE_FAILED)
return
self.set_feature_state(feature, self.FEATURE_STATE_DISABLED)
def resync_feature_state(self, feature):
self._config_db.mod_entry('FEATURE', feature.name, {'state': feature.state})
def set_feature_state(self, feature, state):
self._feature_state_table.set(feature.name, [('state', state)])
class Iptables(object):
def __init__(self):
@ -957,6 +973,7 @@ class HostConfigDaemon:
self.config_db = ConfigDBConnector()
self.config_db.connect(wait_for_init=True, retry_on=True)
self.dbconn = DBConnector(CFG_DB, 0)
self.state_db_conn = DBConnector(STATE_DB, 0)
self.selector = Select()
syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success')
@ -964,6 +981,8 @@ class HostConfigDaemon:
self.callbacks = dict()
self.subscriber_map = dict()
feature_state_table = Table(self.state_db_conn, 'FEATURE')
# Load DEVICE metadata configurations
self.device_config = {}
self.device_config['DEVICE_METADATA'] = self.config_db.get_table('DEVICE_METADATA')
@ -976,7 +995,7 @@ class HostConfigDaemon:
self.iptables = Iptables()
# Intialize Feature Handler
self.feature_handler = FeatureHandler(self.config_db, self.device_config)
self.feature_handler = FeatureHandler(self.config_db, feature_state_table, self.device_config)
self.feature_handler.sync_state_field()
# Initialize Ntp Config Handler

View File

@ -36,6 +36,7 @@ hostcfgd.ConfigDBConnector = MockConfigDb
hostcfgd.SubscriberStateTable = MockSubscriberStateTable
hostcfgd.Select = MockSelect
hostcfgd.DBConnector = MockDBConnector
hostcfgd.Table = mock.Mock()
class TestHostcfgdRADIUS(TestCase):

View File

@ -35,6 +35,7 @@ hostcfgd.ConfigDBConnector = MockConfigDb
hostcfgd.SubscriberStateTable = MockSubscriberStateTable
hostcfgd.Select = MockSelect
hostcfgd.DBConnector = MockDBConnector
hostcfgd.Table = mock.Mock()
class TestHostcfgdTACACS(TestCase):
"""

View File

@ -27,20 +27,23 @@ hostcfgd.ConfigDBConnector = MockConfigDb
hostcfgd.SubscriberStateTable = MockSubscriberStateTable
hostcfgd.Select = MockSelect
hostcfgd.DBConnector = MockDBConnector
hostcfgd.Table = mock.Mock()
class TestHostcfgd(TestCase):
"""
Test hostcfd daemon - feature
"""
def __verify_table(self, table, expected_table):
def __verify_table(self, table, feature_state_table, expected_table):
"""
verify config db tables
Compares Config DB table (FEATURE) with expected output table
Compares Config DB table (FEATURE) with expected output table.
Verifies that State DB table (FEATURE) is updated.
Args:
table(dict): Current Config Db table
feature_state_table(Mock): Mocked State DB FEATURE table
expected_table(dict): Expected Config Db table
Returns:
@ -48,6 +51,19 @@ class TestHostcfgd(TestCase):
"""
ddiff = DeepDiff(table, expected_table, ignore_order=True)
print('DIFF:', ddiff)
def get_state(cfg_state):
""" Translates CONFIG DB state field into STATE DB state field """
if cfg_state == 'always_disabled':
return 'disabled'
elif cfg_state == 'always_enabled':
return 'enabled'
else:
return cfg_state
feature_state_table.set.assert_has_calls([
mock.call(feature, [('state', get_state(table[feature]['state']))]) for feature in table
])
return True if not ddiff else False
def __verify_fs(self, table):
@ -93,6 +109,7 @@ class TestHostcfgd(TestCase):
fs.add_real_paths(swsscommon_package.__path__) # add real path of swsscommon for database_config.json
fs.create_dir(hostcfgd.FeatureHandler.SYSTEMD_SYSTEM_DIR)
MockConfigDb.set_config_db(test_data['config_db'])
feature_state_table_mock = mock.Mock()
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
popen_mock = mock.Mock()
attrs = test_data['popen_attributes']
@ -102,7 +119,7 @@ class TestHostcfgd(TestCase):
# Initialize Feature Handler
device_config = {}
device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA']
feature_handler = hostcfgd.FeatureHandler(MockConfigDb(), device_config)
feature_handler = hostcfgd.FeatureHandler(MockConfigDb(), feature_state_table_mock, device_config)
# sync the state field and Handle Feature Updates
feature_handler.sync_state_field()
@ -113,6 +130,7 @@ class TestHostcfgd(TestCase):
# Verify if the updates are properly updated
assert self.__verify_table(
MockConfigDb.get_config_db()['FEATURE'],
feature_state_table_mock,
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)