Skip to content

Commit 6e45acc

Browse files
authored
Merge pull request #14 from abdosi/feature
What I did: Added Support to render Feature Table using Device running metadata Also added support to render 'has_asic_scope' field of Feature Table. Why I did: Details: sonic-net/sonic-buildimage#11796
2 parents bceb13e + 4d6cad7 commit 6e45acc

File tree

3 files changed

+670
-72
lines changed

3 files changed

+670
-72
lines changed

scripts/hostcfgd

+69-20
Original file line numberDiff line numberDiff line change
@@ -147,30 +147,31 @@ class Feature(object):
147147
"""
148148

149149
self.name = feature_name
150-
self.state = self._get_target_state(feature_cfg.get('state'), device_config or {})
150+
self.state = self._get_feature_table_key_render_value(feature_cfg.get('state'), device_config or {}, ['enabled', 'disabled', 'always_enabled', 'always_disabled'])
151151
self.auto_restart = feature_cfg.get('auto_restart', 'disabled')
152152
self.has_timer = safe_eval(feature_cfg.get('has_timer', 'False'))
153153
self.has_global_scope = safe_eval(feature_cfg.get('has_global_scope', 'True'))
154-
self.has_per_asic_scope = safe_eval(feature_cfg.get('has_per_asic_scope', 'False'))
154+
self.has_per_asic_scope = safe_eval(self._get_feature_table_key_render_value(feature_cfg.get('has_per_asic_scope', 'False'), device_config or {}, ['True', 'False']))
155155

156-
def _get_target_state(self, state_configuration, device_config):
157-
""" Returns the target state for the feature by rendering the state field as J2 template.
156+
def _get_feature_table_key_render_value(self, configuration, device_config, expected_values):
157+
""" Returns the target value for the feature by rendering the configuration as J2 template.
158158
159159
Args:
160-
state_configuration (str): State configuration from CONFIG_DB
161-
deviec_config (dict): DEVICE_METADATA section of CONFIG_DB
160+
configuration (str): Feature Table value from CONFIG_DB for given key
161+
device_config (dict): DEVICE_METADATA section of CONFIG_DB and populated Device Running Metadata
162+
expected_values (list): Expected set of Feature Table value for given key
162163
Returns:
163-
(str): Target feature state
164+
(str): Target feature table value for given key
164165
"""
165166

166-
if state_configuration is None:
167+
if configuration is None:
167168
return None
168169

169-
template = jinja2.Template(state_configuration)
170-
target_state = template.render(device_config)
171-
if target_state not in ('enabled', 'disabled', 'always_enabled', 'always_disabled'):
172-
raise ValueError('Invalid state rendered for feature {}: {}'.format(self.name, target_state))
173-
return target_state
170+
template = jinja2.Template(configuration)
171+
target_value = template.render(device_config)
172+
if target_value not in expected_values:
173+
raise ValueError('Invalid value rendered for feature {}: {}'.format(self.name, target_value))
174+
return target_value
174175

175176
def compare_state(self, feature_name, feature_cfg):
176177
if self.name != feature_name or not isinstance(feature_cfg, dict):
@@ -198,6 +199,7 @@ class FeatureHandler(object):
198199
self._device_config = device_config
199200
self._cached_config = {}
200201
self.is_multi_npu = device_info.is_multi_npu()
202+
self._device_running_config = device_info.get_device_runtime_metadata()
201203

202204
def handler(self, feature_name, op, feature_cfg):
203205
if not feature_cfg:
@@ -206,7 +208,11 @@ class FeatureHandler(object):
206208
self._feature_state_table._del(feature_name)
207209
return
208210

209-
feature = Feature(feature_name, feature_cfg, self._device_config)
211+
device_config = {}
212+
device_config.update(self._device_config)
213+
device_config.update(self._device_running_config)
214+
215+
feature = Feature(feature_name, feature_cfg, device_config)
210216
self._cached_config.setdefault(feature_name, Feature(feature_name, {}))
211217

212218
# Change auto-restart configuration first.
@@ -231,14 +237,18 @@ class FeatureHandler(object):
231237
"""
232238
Summary:
233239
Updates the state field in the FEATURE|* tables as the state field
234-
might have to be rendered based on DEVICE_METADATA table
240+
might have to be rendered based on DEVICE_METADATA table and generated Device Running Metadata
235241
"""
236242
for feature_name in feature_table.keys():
237243
if not feature_name:
238244
syslog.syslog(syslog.LOG_WARNING, "Feature is None")
239245
continue
246+
247+
device_config = {}
248+
device_config.update(self._device_config)
249+
device_config.update(self._device_running_config)
240250

241-
feature = Feature(feature_name, feature_table[feature_name], self._device_config)
251+
feature = Feature(feature_name, feature_table[feature_name], device_config)
242252

243253
self._cached_config.setdefault(feature_name, feature)
244254
self.update_systemd_config(feature)
@@ -284,8 +294,47 @@ class FeatureHandler(object):
284294
self.disable_feature(feature)
285295
syslog.syslog(syslog.LOG_INFO, "Feature {} is stopped and disabled".format(feature.name))
286296

297+
self.sync_feature_asic_scope(feature)
298+
287299
return True
288300

301+
def sync_feature_asic_scope(self, feature_config):
302+
"""Updates the has_per_asic_scope field in the FEATURE|* tables as the field
303+
might have to be rendered based on DEVICE_METADATA table or Device Running configuration.
304+
Disable the ASIC instance service unit file it the render value is False and update config
305+
306+
Args:
307+
feature: An object represents a feature's configuration in `FEATURE`
308+
table of `CONFIG_DB`.
309+
310+
Returns:
311+
None.
312+
"""
313+
314+
cmds = []
315+
feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature_config, True)
316+
for feature_name in feature_names:
317+
if '@' not in feature_name:
318+
continue
319+
unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1]))
320+
if not unit_file_state:
321+
continue
322+
if unit_file_state != "disabled" and not feature_config.has_per_asic_scope:
323+
for suffix in reversed(feature_suffixes):
324+
cmds.append("sudo systemctl stop {}.{}".format(feature_name, suffix))
325+
cmds.append("sudo systemctl disable {}.{}".format(feature_name, feature_suffixes[-1]))
326+
cmds.append("sudo systemctl mask {}.{}".format(feature_name, feature_suffixes[-1]))
327+
for cmd in cmds:
328+
syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd))
329+
try:
330+
run_cmd(cmd, raise_exception=True)
331+
except Exception as err:
332+
syslog.syslog(syslog.LOG_ERR, "Feature '{}.{}' failed to be stopped and disabled"
333+
.format(feature.name, feature_suffixes[-1]))
334+
self.set_feature_state(feature, self.FEATURE_STATE_FAILED)
335+
return
336+
self._config_db.mod_entry('FEATURE', feature_config.name, {'has_per_asic_scope': str(feature_config.has_per_asic_scope)})
337+
289338
def update_systemd_config(self, feature_config):
290339
"""Updates `Restart=` field in feature's systemd configuration file
291340
according to the value of `auto_restart` field in `FEATURE` table of `CONFIG_DB`.
@@ -324,12 +373,12 @@ class FeatureHandler(object):
324373
except Exception as err:
325374
syslog.syslog(syslog.LOG_ERR, "Failed to reload systemd configuration files!")
326375

327-
def get_multiasic_feature_instances(self, feature):
376+
def get_multiasic_feature_instances(self, feature, all_instance=False):
328377
# Create feature name suffix depending feature is running in host or namespace or in both
329378
feature_names = (
330379
([feature.name] if feature.has_global_scope or not self.is_multi_npu else []) +
331380
([(feature.name + '@' + str(asic_inst)) for asic_inst in range(device_info.get_num_npus())
332-
if feature.has_per_asic_scope and self.is_multi_npu])
381+
if self.is_multi_npu and (all_instance or feature.has_per_asic_scope)])
333382
)
334383

335384
if not feature_names:
@@ -359,7 +408,7 @@ class FeatureHandler(object):
359408
for feature_name in feature_names:
360409
# Check if it is already enabled, if yes skip the system call
361410
unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1]))
362-
if unit_file_state == "enabled":
411+
if unit_file_state == "enabled" or not unit_file_state:
363412
continue
364413

365414
for suffix in feature_suffixes:
@@ -389,7 +438,7 @@ class FeatureHandler(object):
389438
for feature_name in feature_names:
390439
# Check if it is already disabled, if yes skip the system call
391440
unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1]))
392-
if unit_file_state in ("disabled", "masked"):
441+
if unit_file_state in ("disabled", "masked") or not unit_file_state:
393442
continue
394443

395444
for suffix in reversed(feature_suffixes):

tests/hostcfgd/hostcfgd_test.py

+74-51
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import sys
33
import swsscommon as swsscommon_package
4+
from sonic_py_common import device_info
45
from swsscommon import swsscommon
56

67
from parameterized import parameterized
@@ -46,7 +47,7 @@ def checks_config_table(self, feature_table, expected_table):
4647

4748
return True if not ddiff else False
4849

49-
def checks_systemd_config_file(self, feature_table):
50+
def checks_systemd_config_file(self, feature_table, feature_systemd_name_map=None):
5051
"""Checks whether the systemd configuration file of each feature was created or not
5152
and whether the `Restart=` field in the file is set correctly or not.
5253
@@ -69,13 +70,16 @@ def checks_systemd_config_file(self, feature_table):
6970
elif "disabled" in auto_restart_status:
7071
auto_restart_status = "disabled"
7172

72-
feature_systemd_config_file_path = systemd_config_file_path.format(feature_name)
73-
is_config_file_existing = os.path.exists(feature_systemd_config_file_path)
74-
assert is_config_file_existing, "Systemd configuration file of feature '{}' does not exist!".format(feature_name)
73+
feature_systemd_list = feature_systemd_name_map[feature_name] if feature_systemd_name_map else [feature_name]
7574

76-
with open(feature_systemd_config_file_path) as systemd_config_file:
77-
status = systemd_config_file.read().strip()
78-
assert status == '[Service]\nRestart={}'.format(truth_table[auto_restart_status])
75+
for feature_systemd in feature_systemd_list:
76+
feature_systemd_config_file_path = systemd_config_file_path.format(feature_systemd)
77+
is_config_file_existing = os.path.exists(feature_systemd_config_file_path)
78+
assert is_config_file_existing, "Systemd configuration file of feature '{}' does not exist!".format(feature_systemd)
79+
80+
with open(feature_systemd_config_file_path) as systemd_config_file:
81+
status = systemd_config_file.read().strip()
82+
assert status == '[Service]\nRestart={}'.format(truth_table[auto_restart_status])
7983

8084
def get_state_db_set_calls(self, feature_table):
8185
"""Returns a Mock call objects which recorded the `set` calls to `FEATURE` table in `STATE_DB`.
@@ -120,31 +124,42 @@ def test_sync_state_field(self, test_scenario_name, config_data, fs):
120124
MockConfigDb.set_config_db(config_data['config_db'])
121125
feature_state_table_mock = mock.Mock()
122126
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
123-
popen_mock = mock.Mock()
124-
attrs = config_data['popen_attributes']
125-
popen_mock.configure_mock(**attrs)
126-
mocked_subprocess.Popen.return_value = popen_mock
127-
128-
device_config = {}
129-
device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA']
130-
feature_handler = hostcfgd.FeatureHandler(MockConfigDb(), feature_state_table_mock, device_config)
131-
132-
feature_table = MockConfigDb.CONFIG_DB['FEATURE']
133-
feature_handler.sync_state_field(feature_table)
134-
135-
is_any_difference = self.checks_config_table(MockConfigDb.get_config_db()['FEATURE'],
136-
config_data['expected_config_db']['FEATURE'])
137-
assert is_any_difference, "'FEATURE' table in 'CONFIG_DB' is modified unexpectedly!"
138-
139-
feature_table_state_db_calls = self.get_state_db_set_calls(feature_table)
140-
141-
self.checks_systemd_config_file(config_data['config_db']['FEATURE'])
142-
mocked_subprocess.check_call.assert_has_calls(config_data['enable_feature_subprocess_calls'],
143-
any_order=True)
144-
mocked_subprocess.check_call.assert_has_calls(config_data['daemon_reload_subprocess_call'],
145-
any_order=True)
146-
feature_state_table_mock.set.assert_has_calls(feature_table_state_db_calls)
147-
self.checks_systemd_config_file(config_data['config_db']['FEATURE'])
127+
with mock.patch("sonic_py_common.device_info.get_device_runtime_metadata", return_value=config_data['device_runtime_metadata']):
128+
with mock.patch("sonic_py_common.device_info.is_multi_npu", return_value=True if 'num_npu' in config_data else False):
129+
with mock.patch("sonic_py_common.device_info.get_num_npus", return_value=config_data['num_npu'] if 'num_npu' in config_data else 1):
130+
popen_mock = mock.Mock()
131+
attrs = config_data['popen_attributes']
132+
popen_mock.configure_mock(**attrs)
133+
mocked_subprocess.Popen.return_value = popen_mock
134+
135+
device_config = {}
136+
device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA']
137+
device_config.update(config_data['device_runtime_metadata'])
138+
139+
feature_handler = hostcfgd.FeatureHandler(MockConfigDb(), feature_state_table_mock, device_config)
140+
141+
feature_table = MockConfigDb.CONFIG_DB['FEATURE']
142+
feature_handler.sync_state_field(feature_table)
143+
144+
feature_systemd_name_map = {}
145+
for feature_name in feature_table.keys():
146+
feature = hostcfgd.Feature(feature_name, feature_table[feature_name], device_config)
147+
feature_names, _ = feature_handler.get_multiasic_feature_instances(feature)
148+
feature_systemd_name_map[feature_name] = feature_names
149+
150+
is_any_difference = self.checks_config_table(MockConfigDb.get_config_db()['FEATURE'],
151+
config_data['expected_config_db']['FEATURE'])
152+
assert is_any_difference, "'FEATURE' table in 'CONFIG_DB' is modified unexpectedly!"
153+
154+
feature_table_state_db_calls = self.get_state_db_set_calls(feature_table)
155+
156+
self.checks_systemd_config_file(config_data['config_db']['FEATURE'], feature_systemd_name_map)
157+
mocked_subprocess.check_call.assert_has_calls(config_data['enable_feature_subprocess_calls'],
158+
any_order=True)
159+
mocked_subprocess.check_call.assert_has_calls(config_data['daemon_reload_subprocess_call'],
160+
any_order=True)
161+
feature_state_table_mock.set.assert_has_calls(feature_table_state_db_calls)
162+
self.checks_systemd_config_file(config_data['config_db']['FEATURE'], feature_systemd_name_map)
148163

149164
@parameterized.expand(HOSTCFGD_TEST_VECTOR)
150165
@patchfs
@@ -165,25 +180,33 @@ def test_handler(self, test_scenario_name, config_data, fs):
165180
MockConfigDb.set_config_db(config_data['config_db'])
166181
feature_state_table_mock = mock.Mock()
167182
with mock.patch('hostcfgd.subprocess') as mocked_subprocess:
168-
popen_mock = mock.Mock()
169-
attrs = config_data['popen_attributes']
170-
popen_mock.configure_mock(**attrs)
171-
mocked_subprocess.Popen.return_value = popen_mock
172-
173-
device_config = {}
174-
device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA']
175-
feature_handler = hostcfgd.FeatureHandler(MockConfigDb(), feature_state_table_mock, device_config)
176-
177-
feature_table = MockConfigDb.CONFIG_DB['FEATURE']
178-
179-
for feature_name, feature_config in feature_table.items():
180-
feature_handler.handler(feature_name, 'SET', feature_config)
181-
182-
self.checks_systemd_config_file(config_data['config_db']['FEATURE'])
183-
mocked_subprocess.check_call.assert_has_calls(config_data['enable_feature_subprocess_calls'],
184-
any_order=True)
185-
mocked_subprocess.check_call.assert_has_calls(config_data['daemon_reload_subprocess_call'],
186-
any_order=True)
183+
with mock.patch("sonic_py_common.device_info.get_device_runtime_metadata", return_value=config_data['device_runtime_metadata']):
184+
with mock.patch("sonic_py_common.device_info.is_multi_npu", return_value=True if 'num_npu' in config_data else False):
185+
with mock.patch("sonic_py_common.device_info.get_num_npus", return_value=config_data['num_npu'] if 'num_npu' in config_data else 1):
186+
popen_mock = mock.Mock()
187+
attrs = config_data['popen_attributes']
188+
popen_mock.configure_mock(**attrs)
189+
mocked_subprocess.Popen.return_value = popen_mock
190+
191+
device_config = {}
192+
device_config['DEVICE_METADATA'] = MockConfigDb.CONFIG_DB['DEVICE_METADATA']
193+
device_config.update(config_data['device_runtime_metadata'])
194+
feature_handler = hostcfgd.FeatureHandler(MockConfigDb(), feature_state_table_mock, device_config)
195+
196+
feature_table = MockConfigDb.CONFIG_DB['FEATURE']
197+
198+
feature_systemd_name_map = {}
199+
for feature_name, feature_config in feature_table.items():
200+
feature_handler.handler(feature_name, 'SET', feature_config)
201+
feature = hostcfgd.Feature(feature_name, feature_table[feature_name], device_config)
202+
feature_names, _ = feature_handler.get_multiasic_feature_instances(feature)
203+
feature_systemd_name_map[feature_name] = feature_names
204+
205+
self.checks_systemd_config_file(config_data['config_db']['FEATURE'], feature_systemd_name_map)
206+
mocked_subprocess.check_call.assert_has_calls(config_data['enable_feature_subprocess_calls'],
207+
any_order=True)
208+
mocked_subprocess.check_call.assert_has_calls(config_data['daemon_reload_subprocess_call'],
209+
any_order=True)
187210

188211
def test_feature_config_parsing(self):
189212
swss_feature = hostcfgd.Feature('swss', {

0 commit comments

Comments
 (0)