Skip to content

Commit bcb2e97

Browse files
committed
hw-mgmt: scripts: Move shared python code to separate module
Moved logger/timer classes from TC files to python module: ./usr/bin/hw_management_lib.py Signed-off-by: Oleksandr Shamray <[email protected]>
1 parent 0a3ad5b commit bcb2e97

File tree

3 files changed

+360
-668
lines changed

3 files changed

+360
-668
lines changed

usr/usr/bin/hw_management_lib.py

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
# Copyright (c) 2019-2024 NVIDIA CORPORATION & AFFILIATES.
2+
# Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
#############################################################################
17+
18+
import os
19+
import sys
20+
import logging
21+
from logging.handlers import RotatingFileHandler, SysLogHandler
22+
from threading import Timer
23+
import time
24+
25+
# ----------------------------------------------------------------------
26+
27+
28+
def str2bool(val):
29+
"""
30+
@summary:
31+
Convert input val value (y/n, true/false, 1/0, y/n) to bool
32+
@param val: input value.
33+
@return: True or False
34+
"""
35+
if isinstance(val, bool):
36+
return val
37+
elif isinstance(val, int):
38+
return bool(val)
39+
elif val.lower() in ("yes", "true", "t", "y", "1"):
40+
return True
41+
elif val.lower() in ("no", "false", "f", "n", "0"):
42+
return False
43+
return None
44+
45+
46+
# ----------------------------------------------------------------------
47+
def current_milli_time():
48+
"""
49+
@summary:
50+
get current time in milliseconds
51+
@return: int value time in milliseconds
52+
"""
53+
return round(time.clock_gettime(1) * 1000)
54+
55+
56+
class SyslogFilter(logging.Filter):
57+
58+
def filter(self, record):
59+
res = False
60+
if record.getMessage().startswith("@syslog "):
61+
record.msg = record.getMessage().replace("@syslog ", "")
62+
res = True
63+
return res
64+
65+
66+
class tc_logger(object):
67+
"""
68+
Logger class provide functionality to log messages.
69+
It can log to several places in parallel
70+
71+
Level When to Use
72+
73+
DEBUG For detailed diagnostic info. Only useful for developers
74+
during debugging.
75+
76+
INFO For normal runtime events. High-level messages showing the
77+
system is working as expected.
78+
79+
NOTICE For important but non-critical events. More significant than `INFO`,
80+
but not a problem.
81+
82+
WARNING For unexpected events that didn't cause a failure, but might.
83+
84+
ERROR For serious issues that caused part of the system to fail.
85+
"""
86+
87+
def __init__(self, use_syslog=False, log_file=None, verbosity=20):
88+
"""
89+
@summary:
90+
The following class provide functionality to log messages.
91+
@param use_syslog: log also to syslog. Applicable arg
92+
value 1-enable/0-disable
93+
@param log_file: log to user specified file. Set '' if no log needed
94+
"""
95+
logging.basicConfig(level=logging.DEBUG)
96+
logging.addLevelName(logging.INFO + 5, "NOTICE")
97+
SysLogHandler.priority_map["NOTICE"] = "notice"
98+
99+
self.logger = logging.getLogger("main")
100+
self.logger.setLevel(logging.DEBUG)
101+
self.logger.propagate = False
102+
self.logger_fh = None
103+
self.logger_emit = True
104+
self.syslog_hash = {} # hash array of the messages which was logged to syslog
105+
106+
self.set_param(use_syslog, log_file, verbosity)
107+
for level in ("debug", "info", "notice", "warn", "error", "critical"):
108+
setattr(self, level, self._make_log_level(level))
109+
110+
def _make_log_level(self, level):
111+
level_map = {
112+
"debug": logging.DEBUG,
113+
"info": logging.INFO,
114+
"notice": logging.INFO + 5,
115+
"warn": logging.WARNING,
116+
"error": logging.ERROR,
117+
"critical": logging.CRITICAL,
118+
}
119+
120+
def log_method(msg, id=None, repeat=0):
121+
self.log_handler(level_map[level], msg, id, repeat)
122+
return log_method
123+
124+
def set_param(self, use_syslog=None, log_file=None, verbosity=20):
125+
"""
126+
@summary:
127+
Set logger parameters. Can be called any time
128+
log provided by /lib/lsb/init-functions always turned on
129+
@param use_syslog: log also to syslog. Applicable arg
130+
value 1-enable/0-disable
131+
@param log_file: log to user specified file. Set None if no log needed
132+
"""
133+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
134+
135+
if log_file:
136+
if any(std_file in log_file for std_file in ["stdout", "stderr"]):
137+
self.logger_fh = logging.StreamHandler()
138+
else:
139+
self.logger_fh = RotatingFileHandler(log_file, maxBytes=(10 * 1024) * 1024, backupCount=3)
140+
141+
self.logger_fh.setFormatter(formatter)
142+
self.logger_fh.setLevel(verbosity)
143+
self.logger.addHandler(self.logger_fh)
144+
145+
if use_syslog:
146+
if sys.platform == "darwin":
147+
address = "/var/run/syslog"
148+
else:
149+
if os.path.exists("/dev/log"):
150+
address = "/dev/log"
151+
else:
152+
address = ("localhost", 514)
153+
facility = SysLogHandler.LOG_SYSLOG
154+
try:
155+
syslog_handler = SysLogHandler(address=address, facility=facility)
156+
syslog_handler.setLevel(logging.INFO)
157+
158+
syslog_handler.setFormatter(logging.Formatter("hw-management-tc: %(levelname)s - %(message)s"))
159+
syslog_handler.addFilter(SyslogFilter("syslog"))
160+
self.logger.addHandler(syslog_handler)
161+
except IOError as err:
162+
print("Can't init syslog {} address {}".format(str(err), address))
163+
164+
def stop(self):
165+
"""
166+
@summary:
167+
Cleanup and Stop logger
168+
"""
169+
logging.shutdown()
170+
handler_list = self.logger.handlers[:]
171+
for handler in handler_list:
172+
handler.close()
173+
self.logger.removeHandler(handler)
174+
self.logger_emit = False
175+
self.syslog_hash = {}
176+
177+
def close_tc_log_handler(self):
178+
if self.logger_fh:
179+
self.logger_fh.flush()
180+
self.logger_fh.close()
181+
self.logger.removeHandler(self.logger_fh)
182+
183+
def set_loglevel(self, verbosity):
184+
"""
185+
@summary:
186+
Set log level for logging in file
187+
@param verbosity: logging level 0 .. 80
188+
"""
189+
if self.logger_fh:
190+
self.logger_fh.setLevel(verbosity)
191+
192+
def log_handler(self, level, msg="", id=None, repeat=0):
193+
"""
194+
@summary:
195+
Logs message to both tc_log and syslog.
196+
1. The message is always logged to tc_log.
197+
2. Repeated messages can be "collapsed" in syslog:
198+
- When a repeated message is detected, it will shown only "repeat" times.
199+
- When the condition clears, a final message with a "clear" marker is logged.
200+
This helps reduce syslog clutter from frequent, identical messages.
201+
@param msg: message text
202+
@param id: unique identifier for the message, used to group and collapse repeats
203+
@param repeat: Maximum number of times to log repeated messages to syslog before collapsing.
204+
"""
205+
# ERROR, WARNING, INFO, NOTICE can be pushed to syslog (optionally)
206+
if level in [logging.ERROR, logging.WARNING, logging.INFO, logging.INFO + 5]:
207+
msg, syslog_emit = self.push_syslog(msg, id, repeat)
208+
# DEBUG can't be pushed to syslog
209+
elif level == logging.DEBUG:
210+
syslog_emit = False
211+
# CRITICAL always push to syslog
212+
elif level == logging.CRITICAL:
213+
syslog_emit = True
214+
else:
215+
raise ValueError(f"Invalid log level: {level}")
216+
217+
if msg:
218+
if not self.logger_emit:
219+
return
220+
self.logger_emit = False
221+
222+
msg_prefix = "@syslog " if syslog_emit else ""
223+
try:
224+
self.logger.log(level, msg_prefix + msg)
225+
except (IOError, OSError, ValueError) as e:
226+
pass
227+
finally:
228+
self.logger_emit = True
229+
230+
def syslog_hash_garbage_collect(self):
231+
"""
232+
@summary:
233+
Remove from syslog_hash all messages older than 60 minutes or if hash is too big
234+
"""
235+
hash_size = len(self.syslog_hash)
236+
self.logger.info("syslog_hash_garbage_collect: hash_size={}".format(hash_size))
237+
if hash_size > 100:
238+
# some major issue. We never expect to have more than 100 messages in hash.
239+
self.logger.error("syslog_hash_garbage_collect: to much ({}) messages in hash. Remove all messages.".format(hash_size))
240+
self.syslog_hash = {}
241+
return
242+
243+
if hash_size > 50:
244+
# some messages was not cleaned up.
245+
# remove messages older than 60 minutes
246+
for id_hash in self.syslog_hash:
247+
if self.syslog_hash[id_hash]["ts"] < current_milli_time() - 60 * 60 * 1000:
248+
self.logger.warning("syslog_hash_garbage_collect: remove message \"{}\" from hash".format(self.syslog_hash[id_hash]["msg"]))
249+
del self.syslog_hash[id_hash]
250+
251+
def push_syslog(self, msg="", id=None, repeat=0):
252+
"""
253+
@param msg: message to save to log
254+
@param id: id used as key for message that should be "collapsed" into start/stop messages
255+
@param repeat: max count of the message to display in syslog
256+
@summary:
257+
if repeat > 0 then message will be logged to syslog "repeat" times.
258+
if id == None just print syslog (no start-stop markers)
259+
if id != None then save to hash, message for log start/stop event
260+
if repeat is 0 stop syslog emmit
261+
if id == None stop syslog emmit
262+
if id != None syslog emmit log with "clear" marker
263+
@return: message to log, syslog_emit flag
264+
"""
265+
266+
syslog_emit = False
267+
id_hash = hash(id) if id else None
268+
269+
if repeat > 0:
270+
syslog_emit = True
271+
if id_hash:
272+
if id_hash in self.syslog_hash:
273+
self.syslog_hash[id_hash]["count"] += 1
274+
self.syslog_hash[id_hash]["msg"] = msg
275+
else:
276+
self.syslog_hash_garbage_collect()
277+
self.syslog_hash[id_hash] = {"count": 1, "msg": msg, "ts": current_milli_time()}
278+
279+
self.syslog_hash[id_hash]["ts"] = current_milli_time()
280+
281+
if self.syslog_hash[id_hash]["count"] > 1:
282+
msg = msg + " (repeated {} times)".format(self.syslog_hash[id_hash]["count"])
283+
284+
if self.syslog_hash[id_hash]["count"] > repeat:
285+
syslog_emit = False
286+
else:
287+
# message in hash - print to syslog last time
288+
if id_hash in self.syslog_hash:
289+
# new message not defined - use message from hash
290+
if not msg:
291+
msg = self.syslog_hash[id_hash]["msg"]
292+
# add "finalization" mark to message
293+
if self.syslog_hash[id_hash]["count"]:
294+
msg = msg + " (clear)"
295+
296+
# remove message from hash
297+
del self.syslog_hash[id_hash]
298+
syslog_emit = True
299+
return msg, syslog_emit
300+
301+
302+
class RepeatedTimer(object):
303+
"""
304+
@summary:
305+
Provide repeat timer service. Can start provided function with selected interval
306+
"""
307+
308+
def __init__(self, interval, function):
309+
"""
310+
@summary:
311+
Create timer object which run function in separate thread
312+
Automatically start timer after init
313+
@param interval: Interval in seconds to run function
314+
@param function: function name to run
315+
"""
316+
self._timer = None
317+
self.interval = interval
318+
self.function = function
319+
320+
self.is_running = False
321+
self.start()
322+
323+
def _run(self):
324+
"""
325+
@summary:
326+
wrapper to run function
327+
"""
328+
self.is_running = False
329+
self.start()
330+
self.function()
331+
332+
def start(self, immediately_run=False):
333+
"""
334+
@summary:
335+
Start selected timer (if it not running)
336+
"""
337+
if immediately_run:
338+
self.function()
339+
self.stop()
340+
341+
if not self.is_running:
342+
self._timer = Timer(self.interval, self._run)
343+
self._timer.start()
344+
self.is_running = True
345+
346+
def stop(self):
347+
"""
348+
@summary:
349+
Stop selected timer (if it started before
350+
"""
351+
self._timer.cancel()
352+
self.is_running = False

0 commit comments

Comments
 (0)