-
Notifications
You must be signed in to change notification settings - Fork 91
/
Copy pathfeatured
594 lines (493 loc) · 28.4 KB
/
featured
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
#!/usr/bin/env python3
import ast
import os
import sys
import subprocess
import syslog
import signal
import jinja2
import time
from sonic_py_common import device_info
from swsscommon.swsscommon import ConfigDBConnector, DBConnector, Table, SonicDBConfig
from swsscommon import swsscommon
# MISC Constants
APPL_DB = "APPL_DB"
CFG_DB = "CONFIG_DB"
STATE_DB = "STATE_DB"
FEATURE_TBL = swsscommon.CFG_FEATURE_TABLE_NAME
PORT_TBL = swsscommon.APP_PORT_TABLE_NAME
HOSTCFGD_MAX_PRI = 10 # Used to enforce ordering b/w daemons under Hostcfgd
DEFAULT_SELECT_TIMEOUT = 1000 # 1sec
PORT_INIT_TIMEOUT_SEC = 180
def run_cmd(cmd, log_err=True, raise_exception=False):
try:
subprocess.check_call(cmd)
except Exception as err:
if log_err:
syslog.syslog(syslog.LOG_ERR, "{} - failed: return code - {}, output:\n{}"
.format(err.cmd, err.returncode, err.output))
if raise_exception:
raise
def signal_handler(sig, frame):
if sig == signal.SIGHUP:
syslog.syslog(syslog.LOG_INFO, "FeatureDaemon: signal 'SIGHUP' is caught and ignoring..")
elif sig == signal.SIGINT:
syslog.syslog(syslog.LOG_INFO, "FeatureDaemon: signal 'SIGINT' is caught and exiting...")
sys.exit(128 + sig)
elif sig == signal.SIGTERM:
syslog.syslog(syslog.LOG_INFO, "FeatureDaemon: signal 'SIGTERM' is caught and exiting...")
sys.exit(128 + sig)
else:
syslog.syslog(syslog.LOG_INFO, "FeatureDaemon: invalid signal - ignoring..")
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
class Feature(object):
""" Represents a feature configuration from CONFIG_DB data. """
def __init__(self, feature_name, feature_cfg, device_config=None):
""" Initialize Feature object based on CONFIG_DB data.
Args:
feature_name (str): Feature name string
feature_cfg (dict): Feature CONFIG_DB configuration
deviec_config (dict): DEVICE_METADATA section of CONFIG_DB
"""
if 'has_timer' in feature_cfg:
err_str = "Invalid obsolete field 'has_timer' in FEATURE table. Please update configuration schema version"
syslog.syslog(syslog.LOG_ERR, err_str)
raise ValueError(err_str)
self.name = feature_name
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.delayed = safe_eval(self._get_feature_table_key_render_value(feature_cfg.get('delayed', 'False'), device_config or {}, ['True', 'False']))
self.has_global_scope = safe_eval(self._get_feature_table_key_render_value(feature_cfg.get('has_global_scope', 'True'), device_config or {}, ['True', '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']))
self.has_per_dpu_scope = safe_eval(feature_cfg.get('has_per_dpu_scope', 'False'))
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:
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 table value for given key
"""
if configuration is None:
return None
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):
return False
if self.state != feature_cfg.get('state', ''):
return False
return True
class FeatureHandler(object):
""" Handles FEATURE table updates. """
SYSTEMD_SYSTEM_DIR = '/etc/systemd/system/'
SYSTEMD_SERVICE_CONF_DIR = os.path.join(SYSTEMD_SYSTEM_DIR, '{}.service.d/')
# 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, is_advanced_boot):
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()
self.is_delayed_enabled = False
self.is_advanced_boot = is_advanced_boot
self._device_running_config = device_info.get_device_runtime_metadata()
self.ns_cfg_db = {}
self.ns_feature_state_tbl = {}
self.num_dpus = device_info.get_num_dpus()
# Initlaize Global config that loads all database*.json
if self.is_multi_npu:
SonicDBConfig.initializeGlobalConfig()
namespaces = device_info.get_namespaces()
for ns in namespaces:
#Connect to ConfigDB in each namespace
self.ns_cfg_db[ns] = ConfigDBConnector(namespace=ns)
self.ns_cfg_db[ns].connect(wait_for_init=True, retry_on=True)
#Connect to stateDB in each namespace
db_conn = DBConnector(STATE_DB, 0, False, ns)
self.ns_feature_state_tbl[ns] = Table(db_conn, FEATURE_TBL)
def enable_delayed_services(self):
self.is_delayed_enabled = True
for feature_name in self._cached_config:
if self._cached_config[feature_name].delayed:
self.update_feature_state(self._cached_config[feature_name])
def handle_adv_boot(self):
if self.is_advanced_boot:
syslog.syslog(syslog.LOG_INFO, "Updating delayed features after warm/fast boot")
self.enable_delayed_services()
def handle_port_table_timeout(self):
if not self.is_delayed_enabled:
syslog.syslog(syslog.LOG_INFO, "Updating delayed features after timeout")
self.enable_delayed_services()
def port_listener(self, key, op, data):
if not key:
return
if op == 'SET' and key == 'PortInitDone':
syslog.syslog(syslog.LOG_INFO, "Updating delayed features after port initialization")
self.enable_delayed_services()
def handler(self, feature_name, op, feature_cfg):
if not feature_cfg:
syslog.syslog(syslog.LOG_INFO, "Deregistering feature {}".format(feature_name))
self._cached_config.pop(feature_name, None)
self._feature_state_table._del(feature_name)
return
device_config = {}
device_config.update(self._device_config)
device_config.update(self._device_running_config)
feature = Feature(feature_name, feature_cfg, device_config)
self._cached_config.setdefault(feature_name, Feature(feature_name, {}))
# Change auto-restart configuration first.
# If service reached failed state before this configuration applies (e.g. on boot)
# 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
# and not auto restart.
if self._cached_config[feature_name].auto_restart != feature.auto_restart:
syslog.syslog(syslog.LOG_INFO, "Auto-restart status of feature '{}' is changed from '{}' to '{}' ..."
.format(feature_name, self._cached_config[feature_name].auto_restart, feature.auto_restart))
self.update_systemd_config(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.
if self._cached_config[feature_name].state != feature.state:
if self.update_feature_state(feature):
self.sync_feature_scope(feature)
self._cached_config[feature_name] = feature
else:
self.resync_feature_state(self._cached_config[feature_name])
def sync_state_field(self, feature_table):
"""
Summary:
Updates the state field in the FEATURE|* tables as the state field
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
device_config = {}
device_config.update(self._device_config)
device_config.update(self._device_running_config)
feature = Feature(feature_name, feature_table[feature_name], device_config)
self._cached_config.setdefault(feature_name, feature)
self.update_systemd_config(feature)
self.update_feature_state(feature)
self.sync_feature_scope(feature)
self.resync_feature_state(feature)
self.sync_feature_delay_state(feature)
def update_feature_state(self, feature):
cached_feature = self._cached_config[feature.name]
enable = False
disable = False
# Allowed transitions:
# None -> always_enabled
# -> always_disabled
# -> enabled
# -> disabled
# always_enabled -> always_disabled
# enabled -> disabled
# disabled -> enabled
if cached_feature.state is None:
enable = feature.state in ("always_enabled", "enabled")
disable = feature.state in ("always_disabled", "disabled")
elif cached_feature.state in ("always_enabled", "always_disabled"):
disable = feature.state == "always_disabled"
enable = feature.state == "always_enabled"
elif cached_feature.state in ("enabled", "disabled"):
enable = feature.state == "enabled"
disable = feature.state == "disabled"
else:
syslog.syslog(syslog.LOG_INFO, "Feature {} service is {}".format(feature.name, cached_feature.state))
return False
if not enable and not disable:
syslog.syslog(syslog.LOG_ERR, "Unexpected state value '{}' for feature {}"
.format(feature.state, feature.name))
return False
if feature.delayed and not self.is_delayed_enabled:
syslog.syslog(syslog.LOG_INFO, "Feature is {} delayed for port init".format(feature.name))
return True
if enable:
self.enable_feature(feature)
syslog.syslog(syslog.LOG_INFO, "Feature {} is enabled and started".format(feature.name))
if disable:
self.disable_feature(feature)
syslog.syslog(syslog.LOG_INFO, "Feature {} is stopped and disabled".format(feature.name))
return True
def sync_feature_scope(self, feature_config):
"""Updates the has_global_scope or 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 Global/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.
"""
feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature_config, True)
for feature_name in feature_names:
unit_file_state = self.get_systemd_unit_state("{}.{}".format(feature_name, feature_suffixes[-1]))
if not unit_file_state:
continue
if not self.is_multi_npu:
continue
if unit_file_state != "masked" and \
((not feature_config.has_per_asic_scope and '@' in feature_name) or (not feature_config.has_global_scope and '@' not in feature_name)):
cmds = []
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_config, self.FEATURE_STATE_FAILED)
return
self._config_db.mod_entry(FEATURE_TBL, feature_config.name, {'has_per_asic_scope': str(feature_config.has_per_asic_scope)})
self._config_db.mod_entry(FEATURE_TBL, feature_config.name, {'has_global_scope': str(feature_config.has_global_scope)})
# sync has_per_asic_scope to CONFIG_DB in namespaces in multi-asic platform
for ns, db in self.ns_cfg_db.items():
db.mod_entry(FEATURE_TBL, feature_config.name, {'has_per_asic_scope': str(feature_config.has_per_asic_scope)})
db.mod_entry(FEATURE_TBL, feature_config.name, {'has_global_scope': str(feature_config.has_global_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`.
Args:
feature: An object represents a feature's configuration in `FEATURE`
table of `CONFIG_DB`.
Returns:
None.
"""
# As per the current code(due to various dependencies) SWSS service stop/start also stops/starts the dependent services(syncd, teamd, bgpd etc)
# There is an issue seen of syncd service getting stopped twice upon a critical process crash in syncd service due to above reason.
# Also early start of syncd service has traffic impact on VOQ chassis.
# to fix the above issue, we are disabling the auto restart of syncd service as it will be started by swss service.
# This change can be extended to other dependent services as well in future and also on pizza box platforms.
device_type = self._device_config.get('DEVICE_METADATA', {}).get('localhost', {}).get('type')
is_dependent_service = feature_config.name in ['syncd', 'gbsyncd']
if device_type == 'SpineRouter' and is_dependent_service:
syslog.syslog(syslog.LOG_INFO, "Skipped setting Restart field in systemd for {}".format(feature_config.name))
restart_field_str = "no"
else:
restart_field_str = "always" if "enabled" in feature_config.auto_restart else "no"
feature_systemd_config = "[Service]\nRestart={}\n".format(restart_field_str)
feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature_config)
# On multi-ASIC device, creates systemd configuration file for each feature instance
# residing in difference namespace.
for feature_name in feature_names:
syslog.syslog(syslog.LOG_INFO, "Updating feature '{}' systemd config file related to auto-restart ..."
.format(feature_name))
feature_systemd_config_dir_path = self.SYSTEMD_SERVICE_CONF_DIR.format(feature_name)
feature_systemd_config_file_path = os.path.join(feature_systemd_config_dir_path, 'auto_restart.conf')
if not os.path.exists(feature_systemd_config_dir_path):
os.mkdir(feature_systemd_config_dir_path)
with open(feature_systemd_config_file_path, 'w') as feature_systemd_config_file_handler:
feature_systemd_config_file_handler.write(feature_systemd_config)
syslog.syslog(syslog.LOG_INFO, "Feature '{}' systemd config file related to auto-restart is updated!"
.format(feature_name))
try:
syslog.syslog(syslog.LOG_INFO, "Reloading systemd configuration files ...")
run_cmd(["sudo", "systemctl", "daemon-reload"], raise_exception=True)
syslog.syslog(syslog.LOG_INFO, "Systemd configuration files are reloaded!")
except Exception as err:
syslog.syslog(syslog.LOG_ERR, "Failed to reload systemd configuration files!")
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 all_instance or not self.is_multi_npu else []) +
([(feature.name + '@' + str(asic_inst)) for asic_inst in range(device_info.get_num_npus())
if self.is_multi_npu and (all_instance or feature.has_per_asic_scope)]) +
([(feature.name + '@' + device_info.DPU_NAME_PREFIX + str(dpu_inst)) for dpu_inst in range(self.num_dpus)
if all_instance or feature.has_per_dpu_scope])
)
if not feature_names:
syslog.syslog(syslog.LOG_ERR, "Feature '{}' service not available"
.format(feature.name))
feature_suffixes = ["service"]
return feature_names, feature_suffixes
def get_systemd_unit_state(self, unit):
""" Returns service configuration """
cmd = ["sudo", "systemctl", "show", unit, "--property", "UnitFileState"]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
syslog.syslog(syslog.LOG_ERR, "Failed to get status of {}: rc={} stderr={}".format(unit, proc.returncode, stderr))
return 'invalid' # same as systemd's "invalid indicates that it could not be determined whether the unit file is enabled".
props = dict([line.split("=") for line in stdout.decode().strip().splitlines()])
return props["UnitFileState"]
def enable_feature(self, feature):
feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature)
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" or not unit_file_state:
continue
cmds = []
for suffix in feature_suffixes:
cmds.append(["sudo", "systemctl", "unmask", "{}.{}".format(feature_name, suffix)])
# If feature has timer associated with it, start/enable corresponding systemd .timer unit
# otherwise, start/enable corresponding systemd .service unit
cmds.append(["sudo", "systemctl", "enable", "{}.{}".format(feature_name, feature_suffixes[-1])])
cmds.append(["sudo", "systemctl", "start", "{}.{}".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 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):
feature_names, feature_suffixes = self.get_multiasic_feature_instances(feature)
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") or not unit_file_state:
continue
cmds = []
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.set_feature_state(feature, self.FEATURE_STATE_DISABLED)
def resync_feature_state(self, feature):
current_entry = self._config_db.get_entry('FEATURE', feature.name)
current_feature_state = current_entry.get('state') if current_entry else None
if feature.state == current_feature_state:
return
# feature.state might be rendered from a template, so that it should resync CONFIG DB
# FEATURE table to override the template value to valid state value
# ('always_enabled', 'always_disabled', 'disabled', 'enabled'). However, we should only
# resync feature state in two cases:
# 1. the rendered feature state is always_enabled or always_disabled, it means that the
# feature state is immutable and potential state change during HostConfigDaemon.load
# in redis should be skipped;
# 2. the current feature state in DB is a template which should be replaced by rendered feature
# state
# For other cases, we should not resync feature.state to CONFIG DB to avoid overriding user configuration.
if self._feature_state_is_immutable(feature.state) or self._feature_state_is_template(current_feature_state):
self._config_db.mod_entry('FEATURE', feature.name, {'state': feature.state})
# resync the feature state to CONFIG_DB in namespaces in multi-asic platform
for ns, db in self.ns_cfg_db.items():
db.mod_entry('FEATURE', feature.name, {'state': feature.state})
def sync_feature_delay_state(self, feature):
current_entry = self._config_db.get_entry('FEATURE', feature.name)
current_feature_delay_state = current_entry.get('delayed') if current_entry else None
if str(feature.delayed) == str(current_feature_delay_state):
return
self._config_db.mod_entry('FEATURE', feature.name, {'delayed': str(feature.delayed)})
for ns, db in self.ns_cfg_db.items():
db.mod_entry('FEATURE', feature.name, {'delayed': str(feature.delayed)})
def set_feature_state(self, feature, state):
self._feature_state_table.set(feature.name, [('state', state)])
# Update the feature state to STATE_DB in namespaces in multi-asic platform
for ns, tbl in self.ns_feature_state_tbl.items():
tbl.set(feature.name, [('state', state)])
def _feature_state_is_template(self, feature_state):
return feature_state not in ('always_enabled', 'always_disabled', 'disabled', 'enabled')
def _feature_state_is_immutable(self, feature_state):
return feature_state in ('always_enabled', 'always_disabled')
class FeatureDaemon:
def __init__(self):
self.cfg_db_conn = DBConnector(CFG_DB, 0)
self.state_db_conn = DBConnector(STATE_DB, 0)
self.appl_db_conn = DBConnector(APPL_DB, 0)
self.callbacks = dict() # selectable <-> callback map
self.subscriber_map = dict() # subscriber <-> fd map
self.advanced_boot = False
if swsscommon.RestartWaiter.isAdvancedBootInProgress(self.state_db_conn):
self.advanced_boot = True
swsscommon.RestartWaiter.waitAdvancedBootDone()
self.config_db = ConfigDBConnector()
self.config_db.connect(wait_for_init=True, retry_on=True)
self.selector = swsscommon.Select()
syslog.syslog(syslog.LOG_INFO, 'ConfigDB connect success')
# Load DEVICE metadata configurations
self.device_config = {}
self.device_config['DEVICE_METADATA'] = self.config_db.get_table('DEVICE_METADATA')
# Load feature state table
feature_state_table = Table(self.state_db_conn, FEATURE_TBL)
# Intialize Feature Handler
self.feature_handler = FeatureHandler(self.config_db, feature_state_table, self.device_config, self.advanced_boot)
self.feature_handler.handle_adv_boot()
def subscribe(self, dbconn, table, callback, pri):
try:
if table not in self.callbacks:
self.callbacks[table] = []
subscriber = swsscommon.SubscriberStateTable(dbconn, table, swsscommon.TableConsumable.DEFAULT_POP_BATCH_SIZE, pri)
self.selector.addSelectable(subscriber) # Add to the Selector
self.subscriber_map[subscriber.getFd()] = (subscriber, table) # Maintain a mapping b/w subscriber & fd
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):
def make_callback(func):
def callback(table, key, op, data):
return func(key, op, data)
return callback
self.subscribe(self.cfg_db_conn, FEATURE_TBL,
make_callback(self.feature_handler.handler), HOSTCFGD_MAX_PRI)
self.subscribe(self.appl_db_conn, PORT_TBL,
make_callback(self.feature_handler.port_listener), HOSTCFGD_MAX_PRI-1)
def render_all_feature_states(self):
features = self.config_db.get_table(FEATURE_TBL)
self.feature_handler.sync_state_field(features)
def start(self, init_time):
while True:
state, selectable_ = self.selector.select(DEFAULT_SELECT_TIMEOUT)
if state == self.selector.TIMEOUT:
if int(time.time() - init_time) > PORT_INIT_TIMEOUT_SEC:
# if the delayed services are not enabled until PORT_INIT_TIMEOUT_SEC, enable them
self.feature_handler.handle_port_table_timeout()
continue
elif state == self.selector.ERROR:
syslog.syslog(syslog.LOG_ERR, "error returned by select")
continue
fd = selectable_.getFd()
# Get the Corresponding subscriber & table
subscriber, table = self.subscriber_map.get(fd, (None, ""))
if not subscriber:
syslog.syslog(syslog.LOG_ERR,
"No Subscriber object found for fd: {}, subscriber map: {}".format(fd, self.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():
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGHUP, signal_handler)
daemon = FeatureDaemon()
init_time = time.time()
daemon.render_all_feature_states()
daemon.register_callbacks()
daemon.start(init_time)
if __name__ == "__main__":
main()