Skip to content

Commit a14c2bb

Browse files
Introduce chassisd to monitor status of cards on chassis (sonic-net#97)
Introducing chassisd to monitor status of cards on a modular chassis HLD: sonic-net/SONiC#646 **-What I did** Introducing a new process to monitor status of control, line and fabric cards. **-How I did it** Support of monitoring of line-cards and fabric-cards. This runs in the main thread periodically. It updates the STATE_DB with the status information. 'show platform chassis-modules' will read from the STATE_DB Support of handling configuration of moving the cards to administratively up/down state. The handling happens as part of a separate thread that waits on select() for config event from a CHASSIS_MODULE table in CONFIG_DB.
1 parent 850d0c6 commit a14c2bb

File tree

10 files changed

+736
-0
lines changed

10 files changed

+736
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@
1010

1111
# Compiled code which doesn't end in '.pyc'
1212
sonic-thermalctld/scripts/thermalctldc
13+
sonic-chassisd/scripts/chassisdc
14+
15+
# Unit test / coverage reports
16+
coverage.xml
17+
.coverage
18+
htmlcov/

sonic-chassisd/pytest.ini

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml

sonic-chassisd/scripts/chassisd

+334
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
chassisd
5+
Module information update daemon for SONiC
6+
This daemon will loop to collect all modules related information and then write the information to state DB.
7+
The loop interval is CHASSIS_INFO_UPDATE_PERIOD_SECS in seconds.
8+
"""
9+
10+
try:
11+
import signal
12+
import sys
13+
import threading
14+
15+
from sonic_py_common import daemon_base, logger
16+
from sonic_py_common.task_base import ProcessTaskBase
17+
except ImportError as e:
18+
raise ImportError (str(e) + " - required module not found")
19+
20+
try:
21+
from swsscommon import swsscommon
22+
except ImportError as e:
23+
from tests import mock_swsscommon as swsscommon
24+
25+
try:
26+
from sonic_platform_base.module_base import ModuleBase
27+
except ImportError as e:
28+
from tests.mock_module_base import ModuleBase
29+
30+
#
31+
# Constants ====================================================================
32+
#
33+
34+
SYSLOG_IDENTIFIER = "chassisd"
35+
36+
CHASSIS_CFG_TABLE = 'CHASSIS_MODULE'
37+
38+
CHASSIS_INFO_TABLE = 'CHASSIS_TABLE'
39+
CHASSIS_INFO_KEY_TEMPLATE = 'CHASSIS {}'
40+
CHASSIS_INFO_CARD_NUM_FIELD = 'module_num'
41+
42+
CHASSIS_MODULE_INFO_TABLE = 'CHASSIS_MODULE_TABLE'
43+
CHASSIS_MODULE_INFO_KEY_TEMPLATE = 'CHASSIS_MODULE {}'
44+
CHASSIS_MODULE_INFO_NAME_FIELD = 'name'
45+
CHASSIS_MODULE_INFO_DESC_FIELD = 'desc'
46+
CHASSIS_MODULE_INFO_SLOT_FIELD = 'slot'
47+
CHASSIS_MODULE_INFO_OPERSTATUS_FIELD = 'oper_status'
48+
49+
CHASSIS_INFO_UPDATE_PERIOD_SECS = 10
50+
51+
CHASSIS_LOAD_ERROR = 1
52+
CHASSIS_NOT_SUPPORTED = 2
53+
54+
platform_chassis = None
55+
56+
SELECT_TIMEOUT = 1000
57+
58+
NOT_AVAILABLE = 'N/A'
59+
INVALID_SLOT = ModuleBase.MODULE_INVALID_SLOT
60+
INVALID_MODULE_INDEX = -1
61+
62+
MODULE_ADMIN_DOWN = 0
63+
MODULE_ADMIN_UP = 1
64+
65+
#
66+
# Helper functions =============================================================
67+
#
68+
69+
# try get information from platform API and return a default value if caught NotImplementedError
70+
def try_get(callback, *args, **kwargs):
71+
"""
72+
Handy function to invoke the callback and catch NotImplementedError
73+
:param callback: Callback to be invoked
74+
:param default: Default return value if exception occur
75+
:return: Default return value if exception occur else return value of the callback
76+
"""
77+
default = kwargs.get('default', NOT_AVAILABLE)
78+
try:
79+
ret = callback(*args)
80+
if ret is None:
81+
ret = default
82+
except NotImplementedError:
83+
ret = default
84+
85+
return ret
86+
87+
#
88+
# Module Config Updater ========================================================
89+
#
90+
class ModuleConfigUpdater(logger.Logger):
91+
92+
def __init__(self, log_identifier, chassis):
93+
"""
94+
Constructor for ModuleConfigUpdater
95+
:param chassis: Object representing a platform chassis
96+
"""
97+
super(ModuleConfigUpdater, self).__init__(log_identifier)
98+
99+
self.chassis = chassis
100+
101+
def deinit(self):
102+
"""
103+
Destructor of ModuleConfigUpdater
104+
:return:
105+
"""
106+
107+
def module_config_update(self, key, admin_state):
108+
if not key.startswith(ModuleBase.MODULE_TYPE_SUPERVISOR) and \
109+
not key.startswith(ModuleBase.MODULE_TYPE_LINE) and \
110+
not key.startswith(ModuleBase.MODULE_TYPE_FABRIC):
111+
self.log_error("Incorrect module-name {}. Should start with {} or {} or {}".format(key,
112+
ModuleBase.MODULE_TYPE_SUPERVISOR, ModuleBase.MODULE_TYPE_LINE,
113+
ModuleBase.MODULE_TYPE_FABRIC))
114+
return
115+
116+
module_index = try_get(self.chassis.get_module_index, key, default=INVALID_MODULE_INDEX)
117+
118+
# Continue if the index is invalid
119+
if module_index < 0:
120+
self.log_error("Unable to get module-index for key {} to set admin-state {}". format(key, admin_state))
121+
return
122+
123+
if (admin_state == MODULE_ADMIN_DOWN) or (admin_state == MODULE_ADMIN_UP):
124+
# Setting the module to administratively up/down state
125+
self.log_info("Changing module {} to admin {} state".format(key,
126+
'DOWN' if admin_state == MODULE_ADMIN_DOWN else 'UP'))
127+
try_get(self.chassis.get_module(module_index).set_admin_state, admin_state, default=False)
128+
129+
#
130+
# Module Updater ==============================================================
131+
#
132+
class ModuleUpdater(logger.Logger):
133+
134+
def __init__(self, log_identifier, chassis):
135+
"""
136+
Constructor for ModuleUpdater
137+
:param chassis: Object representing a platform chassis
138+
"""
139+
super(ModuleUpdater, self).__init__(log_identifier)
140+
141+
self.chassis = chassis
142+
self.num_modules = chassis.get_num_modules()
143+
# Connect to STATE_DB and create chassis info tables
144+
state_db = daemon_base.db_connect("STATE_DB")
145+
self.chassis_table = swsscommon.Table(state_db, CHASSIS_INFO_TABLE)
146+
self.module_table = swsscommon.Table(state_db, CHASSIS_MODULE_INFO_TABLE)
147+
self.info_dict_keys = [CHASSIS_MODULE_INFO_NAME_FIELD,
148+
CHASSIS_MODULE_INFO_DESC_FIELD,
149+
CHASSIS_MODULE_INFO_SLOT_FIELD,
150+
CHASSIS_MODULE_INFO_OPERSTATUS_FIELD]
151+
152+
def deinit(self):
153+
"""
154+
Destructor of ModuleUpdater
155+
:return:
156+
"""
157+
# Delete all the information from DB and then exit
158+
for module_index in range(0, self.num_modules):
159+
name = try_get(self.chassis.get_module(module_index).get_name)
160+
self.module_table._del(name)
161+
162+
if self.chassis_table is not None:
163+
self.chassis_table._del(CHASSIS_INFO_KEY_TEMPLATE.format(1))
164+
165+
def modules_num_update(self):
166+
# Check if module list is populated
167+
num_modules = self.chassis.get_num_modules()
168+
if num_modules == 0:
169+
self.log_error("Chassisd has no modules available")
170+
return
171+
172+
# Post number-of-modules info to STATE_DB
173+
fvs = swsscommon.FieldValuePairs([(CHASSIS_INFO_CARD_NUM_FIELD, str(num_modules))])
174+
self.chassis_table.set(CHASSIS_INFO_KEY_TEMPLATE.format(1), fvs)
175+
176+
def module_db_update(self):
177+
for module_index in range(0, self.num_modules):
178+
module_info_dict = self._get_module_info(module_index)
179+
if module_info_dict is not None:
180+
key = module_info_dict[CHASSIS_MODULE_INFO_NAME_FIELD]
181+
182+
if not key.startswith(ModuleBase.MODULE_TYPE_SUPERVISOR) and \
183+
not key.startswith(ModuleBase.MODULE_TYPE_LINE) and \
184+
not key.startswith(ModuleBase.MODULE_TYPE_FABRIC):
185+
self.log_error("Incorrect module-name {}. Should start with {} or {} or {}".format(key,
186+
ModuleBase.MODULE_TYPE_SUPERVISOR, ModuleBase.MODULE_TYPE_LINE,
187+
ModuleBase.MODULE_TYPE_FABRIC))
188+
continue
189+
190+
fvs = swsscommon.FieldValuePairs([(CHASSIS_MODULE_INFO_DESC_FIELD, module_info_dict[CHASSIS_MODULE_INFO_DESC_FIELD]),
191+
(CHASSIS_MODULE_INFO_SLOT_FIELD, module_info_dict[CHASSIS_MODULE_INFO_SLOT_FIELD]),
192+
(CHASSIS_MODULE_INFO_OPERSTATUS_FIELD, module_info_dict[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD])])
193+
self.module_table.set(key, fvs)
194+
195+
def _get_module_info(self, module_index):
196+
"""
197+
Retrieves module info of this module
198+
"""
199+
module_info_dict = {}
200+
module_info_dict = dict.fromkeys(self.info_dict_keys, 'N/A')
201+
name = try_get(self.chassis.get_module(module_index).get_name)
202+
desc = try_get(self.chassis.get_module(module_index).get_description)
203+
slot = try_get(self.chassis.get_module(module_index).get_slot, default=INVALID_SLOT)
204+
status = try_get(self.chassis.get_module(module_index).get_oper_status, default=ModuleBase.MODULE_STATUS_OFFLINE)
205+
206+
module_info_dict[CHASSIS_MODULE_INFO_NAME_FIELD] = name
207+
module_info_dict[CHASSIS_MODULE_INFO_DESC_FIELD] = str(desc)
208+
module_info_dict[CHASSIS_MODULE_INFO_SLOT_FIELD] = str(slot)
209+
module_info_dict[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] = str(status)
210+
211+
return module_info_dict
212+
213+
#
214+
# Config Manager task ========================================================
215+
#
216+
class ConfigManagerTask(ProcessTaskBase):
217+
def __init__(self):
218+
ProcessTaskBase.__init__(self)
219+
220+
# TODO: Refactor to eliminate the need for this Logger instance
221+
self.logger = logger.Logger(SYSLOG_IDENTIFIER)
222+
223+
def task_worker(self):
224+
self.config_updater = ModuleConfigUpdater(SYSLOG_IDENTIFIER, platform_chassis)
225+
config_db = daemon_base.db_connect("CONFIG_DB")
226+
227+
# Subscribe to CHASSIS_MODULE table notifications in the Config DB
228+
sel = swsscommon.Select()
229+
sst = swsscommon.SubscriberStateTable(config_db, CHASSIS_CFG_TABLE)
230+
sel.addSelectable(sst)
231+
232+
# Listen indefinitely for changes to the CFG_CHASSIS_MODULE_TABLE table in the Config DB
233+
while True:
234+
# Use timeout to prevent ignoring the signals we want to handle
235+
# in signal_handler() (e.g. SIGTERM for graceful shutdown)
236+
(state, c) = sel.select(SELECT_TIMEOUT)
237+
238+
if state == swsscommon.Select.TIMEOUT:
239+
# Do not flood log when select times out
240+
continue
241+
if state != swsscommon.Select.OBJECT:
242+
self.logger.log_warning("sel.select() did not return swsscommon.Select.OBJECT")
243+
continue
244+
245+
(key, op, fvp) = sst.pop()
246+
247+
if op == 'SET':
248+
admin_state = MODULE_ADMIN_DOWN
249+
elif op == 'DEL':
250+
admin_state = MODULE_ADMIN_UP
251+
else:
252+
continue
253+
254+
self.config_updater.module_config_update(key, admin_state)
255+
256+
#
257+
# Daemon =======================================================================
258+
#
259+
260+
class ChassisdDaemon(daemon_base.DaemonBase):
261+
def __init__(self, log_identifier):
262+
super(ChassisdDaemon, self).__init__(log_identifier)
263+
264+
self.stop = threading.Event()
265+
266+
# Signal handler
267+
def signal_handler(self, sig, frame):
268+
if sig == signal.SIGHUP:
269+
self.log_info("Caught SIGHUP - ignoring...")
270+
elif sig == signal.SIGINT:
271+
self.log_info("Caught SIGINT - exiting...")
272+
self.stop.set()
273+
elif sig == signal.SIGTERM:
274+
self.log_info("Caught SIGTERM - exiting...")
275+
self.stop.set()
276+
else:
277+
self.log_warning("Caught unhandled signal '" + sig + "'")
278+
279+
# Run daemon
280+
def run(self):
281+
global platform_chassis
282+
283+
self.log_info("Starting up...")
284+
285+
# Load new platform api class
286+
try:
287+
import sonic_platform.platform
288+
platform_chassis = sonic_platform.platform.Platform().get_chassis()
289+
except Exception as e:
290+
self.log_error("Failed to load chassis due to {}".format(repr(e)))
291+
sys.exit(CHASSIS_LOAD_ERROR)
292+
293+
# Check if module list is populated
294+
self.module_updater = ModuleUpdater(SYSLOG_IDENTIFIER, platform_chassis)
295+
self.module_updater.modules_num_update()
296+
297+
# Check for valid slot numbers
298+
my_slot = try_get(platform_chassis.get_my_slot, default=INVALID_SLOT)
299+
supervisor_slot = try_get(platform_chassis.get_supervisor_slot, default=INVALID_SLOT)
300+
if (my_slot == INVALID_SLOT) or (supervisor_slot == INVALID_SLOT):
301+
self.log_error("Chassisd not supported for this platform")
302+
sys.exit(CHASSIS_NOT_SUPPORTED)
303+
304+
# Start configuration manager task on supervisor module
305+
if supervisor_slot == my_slot:
306+
config_manager = ConfigManagerTask()
307+
config_manager.task_run()
308+
309+
# Start main loop
310+
self.log_info("Start daemon main loop")
311+
312+
while not self.stop.wait(CHASSIS_INFO_UPDATE_PERIOD_SECS):
313+
self.module_updater.module_db_update()
314+
315+
self.log_info("Stop daemon main loop")
316+
317+
if config_manager is not None:
318+
config_manager.task_stop()
319+
320+
# Delete all the information from DB and then exit
321+
self.module_updater.deinit()
322+
323+
self.log_info("Shutting down...")
324+
325+
#
326+
# Main =========================================================================
327+
#
328+
329+
def main():
330+
chassisd = ChassisdDaemon(SYSLOG_IDENTIFIER)
331+
chassisd.run()
332+
333+
if __name__ == '__main__':
334+
main()

sonic-chassisd/setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[aliases]
2+
test=pytest

sonic-chassisd/setup.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from setuptools import setup
2+
3+
setup(
4+
name='sonic-chassisd',
5+
version='1.0',
6+
description='Chassis daemon for SONiC',
7+
license='Apache 2.0',
8+
author='SONiC Team',
9+
author_email='[email protected]',
10+
url='https://github.com/Azure/sonic-platform-daemons',
11+
maintainer='Manju Prabhu',
12+
maintainer_email='[email protected]',
13+
packages=[
14+
'tests'
15+
],
16+
scripts=[
17+
'scripts/chassisd',
18+
],
19+
setup_requires= [
20+
'pytest-runner',
21+
'wheel'
22+
],
23+
tests_require = [
24+
'pytest',
25+
'mock>=2.0.0',
26+
'pytest-cov'
27+
],
28+
classifiers=[
29+
'Development Status :: 4 - Beta',
30+
'Environment :: No Input/Output (Daemon)',
31+
'Intended Audience :: Developers',
32+
'Intended Audience :: Information Technology',
33+
'Intended Audience :: System Administrators',
34+
'License :: OSI Approved :: Apache Software License',
35+
'Natural Language :: English',
36+
'Operating System :: POSIX :: Linux',
37+
'Programming Language :: Python :: 2.7',
38+
'Topic :: System :: Hardware',
39+
],
40+
keywords='sonic SONiC chassis Chassis daemon chassisd',
41+
test_suite='setup.get_test_suite'
42+
)

sonic-chassisd/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)