diff --git a/scripts/hostcfgd b/scripts/hostcfgd index a82a630bfc0a..1026527585bf 100755 --- a/scripts/hostcfgd +++ b/scripts/hostcfgd @@ -146,30 +146,31 @@ class Feature(object): """ self.name = feature_name - self.state = self._get_target_state(feature_cfg.get('state'), device_config or {}) + self.state = self._get_feature_table_key_render_value(feature_cfg.get('state'), device_config or {}, ['enabled', 'disabled', 'always_enabled', 'always_disabled']) self.auto_restart = feature_cfg.get('auto_restart', 'disabled') self.has_timer = safe_eval(feature_cfg.get('has_timer', 'False')) self.has_global_scope = safe_eval(feature_cfg.get('has_global_scope', 'True')) - self.has_per_asic_scope = safe_eval(feature_cfg.get('has_per_asic_scope', 'False')) + 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'])) - def _get_target_state(self, state_configuration, device_config): - """ Returns the target state for the feature by rendering the state field as J2 template. + def _get_feature_table_key_render_value(self, configuration, device_config, expected_values): + """ Returns the target value for the feature by rendering the configuration as J2 template. Args: - state_configuration (str): State configuration from CONFIG_DB - deviec_config (dict): DEVICE_METADATA section of CONFIG_DB + configuration (str): Feature Table value from CONFIG_DB for given key + device_config (dict): DEVICE_METADATA section of CONFIG_DB and populated Device Running Metadata + expected_values (list): Expected set of Feature Table value for given key Returns: - (str): Target feature state + (str): Target feature table value for given key """ - if state_configuration is None: + if configuration is None: return None - template = jinja2.Template(state_configuration) - target_state = template.render(device_config) - if target_state not in ('enabled', 'disabled', 'always_enabled', 'always_disabled'): - raise ValueError('Invalid state rendered for feature {}: {}'.format(self.name, target_state)) - return target_state + template = jinja2.Template(configuration) + target_value = template.render(device_config) + if target_value not in expected_values: + raise ValueError('Invalid value rendered for feature {}: {}'.format(self.name, target_value)) + return target_value def compare_state(self, feature_name, feature_cfg): if self.name != feature_name or not isinstance(feature_cfg, dict): @@ -197,6 +198,7 @@ class FeatureHandler(object): self._device_config = device_config self._cached_config = {} self.is_multi_npu = device_info.is_multi_npu() + self._device_running_config = device_info.get_device_runtime_metadata() def handler(self, feature_name, op, feature_cfg): if not feature_cfg: @@ -205,7 +207,7 @@ class FeatureHandler(object): self._feature_state_table._del(feature_name) return - feature = Feature(feature_name, feature_cfg, self._device_config) + feature = Feature(feature_name, feature_cfg, self._device_config | self._device_running_config) self._cached_config.setdefault(feature_name, Feature(feature_name, {})) # Change auto-restart configuration first. @@ -230,14 +232,14 @@ class FeatureHandler(object): """ Summary: Updates the state field in the FEATURE|* tables as the state field - might have to be rendered based on DEVICE_METADATA table + might have to be rendered based on DEVICE_METADATA table and generated Device Running Metadata """ for feature_name in feature_table.keys(): if not feature_name: syslog.syslog(syslog.LOG_WARNING, "Feature is None") continue - feature = Feature(feature_name, feature_table[feature_name], self._device_config) + feature = Feature(feature_name, feature_table[feature_name], self._device_config | self._device_running_config) self._cached_config.setdefault(feature_name, feature) self.update_systemd_config(feature) @@ -283,8 +285,50 @@ class FeatureHandler(object): self.disable_feature(feature) syslog.syslog(syslog.LOG_INFO, "Feature {} is stopped and disabled".format(feature.name)) + if self.is_multi_npu: + self.sync_feature_asic_scope(feature) + return True + def sync_feature_asic_scope(self, feature_config): + """Updates the has_per_asic_scope field in the FEATURE|* tables as the field + might have to be rendered based on DEVICE_METADATA table or Device Running configuration. + Disable the ASIC instance service unit file it the render value is False and update config + + Args: + feature: An object represents a feature's configuration in `FEATURE` + table of `CONFIG_DB`. + + Returns: + None. + """ + + cmds = [] + feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature_config, True) + for feature_name in feature_names: + if '@' not in feature_name: + continue + unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1])) + if not unit_file_state: + continue + if unit_file_state == "enabled" and not feature_config.has_per_asic_scope: + for suffix in reversed(feature_suffixes): + cmds.append("sudo systemctl stop {}.{}".format(feature_name, suffix)) + cmds.append("sudo systemctl disable {}.{}".format(feature_name, feature_suffixes[-1])) + cmds.append("sudo systemctl mask {}.{}".format(feature_name, feature_suffixes[-1])) + for cmd in cmds: + syslog.syslog(syslog.LOG_INFO, "Running cmd: '{}'".format(cmd)) + try: + run_cmd(cmd, raise_exception=True) + 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._config_db.mod_entry('FEATURE', feature_config.name, {'has_per_asic_scope': feature_config.has_per_asic_scope}) + + + def update_systemd_config(self, feature_config): """Updates `Restart=` field in feature's systemd configuration file according to the value of `auto_restart` field in `FEATURE` table of `CONFIG_DB`. @@ -323,12 +367,12 @@ class FeatureHandler(object): except Exception as err: syslog.syslog(syslog.LOG_ERR, "Failed to reload systemd configuration files!") - def get_multiasic_feature_instances(self, feature): + def get_multiasic_feature_instances(self, feature, all_instance=False): # Create feature name suffix depending feature is running in host or namespace or in both feature_names = ( ([feature.name] if feature.has_global_scope or not self.is_multi_npu else []) + ([(feature.name + '@' + str(asic_inst)) for asic_inst in range(device_info.get_num_npus()) - if feature.has_per_asic_scope and self.is_multi_npu]) + if self.is_multi_npu and (all_instance or feature.has_per_asic_scope)]) ) if not feature_names: @@ -358,7 +402,7 @@ class FeatureHandler(object): for feature_name in feature_names: # Check if it is already enabled, if yes skip the system call unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1])) - if unit_file_state == "enabled": + if unit_file_state == "enabled" or not unit_file_state: continue for suffix in feature_suffixes: @@ -388,7 +432,7 @@ class FeatureHandler(object): for feature_name in feature_names: # Check if it is already disabled, if yes skip the system call unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1])) - if unit_file_state in ("disabled", "masked"): + if unit_file_state in ("disabled", "masked") or not unit_file_state: continue for suffix in reversed(feature_suffixes):