]> git.proxmox.com Git - mirror_ifupdown2.git/blob - ifupdown2/lib/log.py
ifupdown2.conf: add persistent debug logging feature
[mirror_ifupdown2.git] / ifupdown2 / lib / log.py
1 # Copyright (C) 2016, 2017, 2018, 2019 Cumulus Networks, Inc. all rights reserved
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License as
5 # published by the Free Software Foundation; version 2.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10 # General Public License for more details.
11 #
12 # You should have received a copy of the GNU General Public License
13 # along with this program; if not, write to the Free Software
14 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
15 # 02110-1301, USA.
16 #
17 # https://www.gnu.org/licenses/gpl-2.0-standalone.html
18 #
19 # Author:
20 # Julien Fortin, julien@cumulusnetworks.com
21 #
22
23 import os
24 import sys
25 import shutil
26 import traceback
27
28 import logging
29 import logging.handlers
30
31 from datetime import date, datetime
32
33 try:
34 from ifupdown2.ifupdown.utils import utils
35 except:
36 from ifupdown.utils import utils
37
38
39 root_logger = logging.getLogger()
40
41
42 class LogManager:
43 LOGGER_NAME = "ifupdown2"
44 LOGGER_NAME_DAEMON = "ifupdown2d"
45
46 LOGGING_DIRECTORY = "/etc/network/ifupdown2/log"
47 LOGGING_DIRECTORY_PREFIX = "network_config_ifupdown2_"
48 LOGGING_DIRECTORY_LIMIT = 42
49
50 DEFAULT_TCP_LOGGING_PORT = 42422
51 DEFAULT_LOGGING_LEVEL_DAEMON = logging.INFO
52 DEFAULT_LOGGING_LEVEL_NORMAL = logging.WARNING
53
54 __instance = None
55
56 @staticmethod
57 def get_instance():
58 if not LogManager.__instance:
59 try:
60 LogManager.__instance = LogManager()
61 except Exception as e:
62 sys.stderr.write("warning: ifupdown2.Log: %s\n" % str(e))
63 traceback.print_exc()
64 return LogManager.__instance
65
66 def __init__(self):
67 """
68 Setup root logger and console handler (stderr). To enable daemon, client
69 or standalone logging please call the proper function, see:
70 "start_(daemon|client|standlone)_logging"
71 """
72 if LogManager.__instance:
73 raise RuntimeError("Log: invalid access. Please use Log.getInstance()")
74 else:
75 LogManager.__instance = self
76
77 self.__fmt = "%(levelname)s: %(message)s"
78
79 self.__debug_fmt = "%(asctime)s: %(threadName)s: %(name)s: " \
80 "%(filename)s:%(lineno)d:%(funcName)s(): " \
81 "%(levelname)s: %(message)s"
82
83 self.__root_logger = logging.getLogger()
84 self.__root_logger.name = self.LOGGER_NAME
85
86 self.__debug_handler = None
87 self.__socket_handler = None
88 self.__syslog_handler = None
89 self.__console_handler = None
90
91 self.daemon = None
92
93 # by default we attach a console handler that logs on stderr
94 # the daemon can manually remove this handler on startup
95 self.__console_handler = logging.StreamHandler(sys.stderr)
96 self.__console_handler.setFormatter(logging.Formatter(self.__fmt))
97 self.__console_handler.setLevel(logging.INFO)
98
99 self.__root_logger.addHandler(self.__console_handler)
100
101 if os.path.exists("/dev/log"):
102 try:
103 self.__syslog_handler = logging.handlers.SysLogHandler(
104 address="/dev/log",
105 facility=logging.handlers.SysLogHandler.LOG_DAEMON
106 )
107 self.__syslog_handler.setFormatter(logging.Formatter(self.__fmt))
108 except Exception as e:
109 sys.stderr.write("warning: syslog: %s\n" % str(e))
110 self.__syslog_handler = None
111
112 logging.addLevelName(logging.CRITICAL, "critical")
113 logging.addLevelName(logging.WARNING, "warning")
114 logging.addLevelName(logging.ERROR, "error")
115 logging.addLevelName(logging.DEBUG, "debug")
116 logging.addLevelName(logging.INFO, "info")
117
118 try:
119 self.__init_debug_logging()
120 except Exception as e:
121 self.__root_logger.debug("couldn't initialize persistent debug logging: %s" % str(e))
122
123 def __get_enable_persistent_debug_logging(self):
124 # ifupdownconfig.config is not yet initialized so we need to cat and grep ifupdown2.conf
125 # by default we limit logging to LOGGING_DIRECTORY_LIMIT number of files
126 # the user can specify a different amount in /etc/network/ifupdown2/ifupdown2.conf
127 # or just yes/no to enable/disable the feature.
128 try:
129 user_config_limit_str = (
130 utils.exec_user_command(
131 "cat /etc/network/ifupdown2/ifupdown2.conf | grep enable_persistent_debug_logging") or ""
132 ).strip().split("=", 1)[1]
133
134 try:
135 # get the integer amount
136 return int(user_config_limit_str)
137 except ValueError:
138 # the user didn't specify an integer but a boolean
139 # if the input is not recognized we are disabling the feature
140 user_config_limit = {
141 True: self.LOGGING_DIRECTORY_LIMIT,
142 False: 0,
143 }.get(utils.get_boolean_from_string(user_config_limit_str))
144
145 except Exception:
146 user_config_limit = self.LOGGING_DIRECTORY_LIMIT
147
148 return user_config_limit
149
150 def __init_debug_logging(self):
151 # check if enable_persistent_debug_logging is enabled
152 user_config_limit = self.__get_enable_persistent_debug_logging()
153
154 if not user_config_limit:
155 # user has disabled the feature
156 return
157
158 # create logging directory
159 self.__create_dir(self.LOGGING_DIRECTORY)
160
161 # list all ifupdown2 logging directories
162 ifupdown2_log_dirs = [
163 directory[len(self.LOGGING_DIRECTORY_PREFIX):].split("_", 1) for directory in os.listdir(self.LOGGING_DIRECTORY) if directory.startswith(self.LOGGING_DIRECTORY_PREFIX)
164 ]
165 ifupdown2_log_dirs.sort(key=lambda x: int(x[0]))
166
167 # get the last log id
168 if ifupdown2_log_dirs:
169 last_id = int(ifupdown2_log_dirs[-1][0])
170 else:
171 last_id = 0
172
173 # create new log directory to store eni and debug logs
174 # format: network_config_ifupdown2_1_Aug-17-2021_23:42:00.000000
175 new_dir_path = "%s/%s%s_%s" % (
176 self.LOGGING_DIRECTORY,
177 self.LOGGING_DIRECTORY_PREFIX,
178 last_id + 1,
179 "%s_%s" % (date.today().strftime("%b-%d-%Y"), str(datetime.now()).split(" ", 1)[1])
180 )
181 self.__create_dir(new_dir_path)
182
183 # start logging in the new directory
184 self.__debug_handler = logging.FileHandler("%s/ifupdown2.debug.log" % new_dir_path, mode="w+")
185 self.__debug_handler.setFormatter(logging.Formatter(self.__debug_fmt))
186 self.__debug_handler.setLevel(logging.DEBUG)
187
188 self.__root_logger.addHandler(self.__debug_handler)
189 self.__root_logger.setLevel(logging.DEBUG)
190
191 self.__root_logger.debug("persistent debugging is initialized")
192
193 # cp ENI and ENI.d in the log directory
194 shutil.copy2("/etc/network/interfaces", new_dir_path)
195 try:
196 shutil.copytree("/etc/network/interfaces.d/", new_dir_path)
197 except FileNotFoundError:
198 pass
199
200 # remove extra directory logs if we are reaching the 'user_config_limit'
201 len_ifupdown2_log_dirs = len(ifupdown2_log_dirs)
202 if len_ifupdown2_log_dirs > user_config_limit:
203 for index in range(0, len_ifupdown2_log_dirs - user_config_limit):
204 directory_to_remove = "%s/%s%s_%s" % (self.LOGGING_DIRECTORY, self.LOGGING_DIRECTORY_PREFIX, ifupdown2_log_dirs[index][0], ifupdown2_log_dirs[index][1])
205 shutil.rmtree(directory_to_remove, ignore_errors=True)
206
207 @staticmethod
208 def __create_dir(path):
209 if not os.path.isdir(path):
210 os.mkdir(path, mode=0o400)
211
212 def set_level(self, default, error=False, warning=False, info=False, debug=False):
213 """
214 Set root handler logging level
215 :param default:
216 :param error:
217 :param warning:
218 :param info:
219 :param debug:
220 """
221 if debug:
222 log_level = logging.DEBUG
223 elif info:
224 log_level = logging.INFO
225 elif warning:
226 log_level = logging.WARNING
227 elif error:
228 log_level = logging.ERROR
229 else:
230 log_level = default
231
232 for handler in self.__root_logger.handlers:
233 if handler == self.__debug_handler:
234 continue
235 handler.setLevel(log_level)
236
237 # make sure that the root logger has the lowest logging level possible
238 # otherwise some messages might not go through
239 if self.__root_logger.level > log_level:
240 self.__root_logger.setLevel(log_level)
241
242 def enable_console(self):
243 """ Add console handler to root logger """
244 self.__root_logger.addHandler(self.__console_handler)
245
246 def disable_console(self):
247 """ Remove console handler from root logger """
248 self.__root_logger.removeHandler(self.__console_handler)
249
250 def enable_syslog(self):
251 """ Add syslog handler to root logger """
252 if self.__syslog_handler and self.__syslog_handler not in self.__root_logger.handlers:
253 self.__root_logger.addHandler(self.__syslog_handler)
254
255 def disable_syslog(self):
256 """ Remove syslog handler from root logger """
257 if self.__syslog_handler:
258 self.__root_logger.removeHandler(self.__syslog_handler)
259
260 def is_syslog_enabled(self):
261 return self.__syslog_handler in self.__root_logger.handlers
262
263 def get_syslog_log_level(self):
264 return self.__syslog_handler.level if self.__syslog_handler else None
265
266 def set_level_syslog(self, level):
267 if self.__syslog_handler:
268 self.__syslog_handler.setLevel(level)
269
270 if self.__root_logger.level > level:
271 self.__root_logger.setLevel(level)
272
273 def close_log_stream(self):
274 """ Close socket to disconnect client.
275 We first have to perform this little hack: it seems like the socket is
276 not opened until data (LogRecord) are transmitted. In our most basic use
277 case (client sends "ifup -a") the daemon doesn't send back any LogRecord
278 but we can't predict that in the client. The client is already in a
279 blocking-select waiting for data on it's socket handler
280 (StreamRequestHandler). For this special case we need to manually call
281 "createSocket" to open the channel to the client so that we can properly
282 close it. That way the client can exit cleanly.
283 """
284 self.__root_logger.removeHandler(self.__socket_handler)
285 self.__socket_handler.acquire()
286 self.__socket_handler.retryTime = None
287 try:
288 if not self.__socket_handler.sock:
289 self.__socket_handler.createSocket()
290 finally:
291 self.__socket_handler.close()
292 self.__socket_handler.release()
293
294 def start_stream(self):
295 self.__root_logger.addHandler(self.__socket_handler)
296
297 def set_daemon_logging_level(self, args):
298 self.set_level(self.DEFAULT_LOGGING_LEVEL_DAEMON, info=args.verbose, debug=args.debug)
299
300 def set_request_logging_level(self, args):
301 if not hasattr(args, "syslog") or not args.syslog:
302 self.disable_syslog()
303 else:
304 self.__root_logger.removeHandler(self.__socket_handler)
305 self.set_level(self.DEFAULT_LOGGING_LEVEL_NORMAL, info=args.verbose, debug=args.debug)
306
307 def start_client_logging(self, args):
308 """ Setup root logger name and client log level
309 syslog is handled by the daemon directly
310 """
311 self.__root_logger.name = self.LOGGER_NAME
312
313 if hasattr(args, "syslog") and args.syslog:
314 self.enable_syslog()
315 self.disable_console()
316
317 self.set_level(self.DEFAULT_LOGGING_LEVEL_NORMAL, info=args.verbose, debug=args.debug)
318
319 def start_standalone_logging(self, args):
320 self.__root_logger.name = self.LOGGER_NAME
321
322 if hasattr(args, "syslog") and args.syslog:
323 self.enable_syslog()
324 self.disable_console()
325
326 self.__root_logger.removeHandler(self.__console_handler)
327
328 self.set_level(self.DEFAULT_LOGGING_LEVEL_NORMAL, info=args.verbose, debug=args.debug)
329
330 def start_daemon_logging(self, args):
331 """
332 Daemon mode initialize a socket handler to transmit logging to the
333 client, we can also do syslog logging and/or console logging (probably
334 just for debugging purpose)
335 :param args:
336 :return:
337 """
338 self.__root_logger.name = self.LOGGER_NAME_DAEMON
339 self.daemon = True
340
341 self.enable_syslog()
342
343 # Create SocketHandler for daemon-client communication
344 self.__socket_handler = logging.handlers.SocketHandler(
345 "localhost",
346 port=self.DEFAULT_TCP_LOGGING_PORT
347 )
348 self.__root_logger.addHandler(self.__socket_handler)
349
350 if not args.console:
351 self.disable_console()
352
353 self.set_daemon_logging_level(args)
354
355 def write(self, msg):
356 root_logger.info(msg)
357
358 def root_logger(self):
359 return self.__root_logger