]> git.proxmox.com Git - mirror_ifupdown2.git/blob - ifupdown2/lib/log.py
log: use default chmod for os.mkdir
[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 = "/var/log/ifupdown2"
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/", "%s/interfaces.d" % new_dir_path)
197 except Exception:
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 try:
205 directory_to_remove = "%s/%s%s_%s" % (self.LOGGING_DIRECTORY, self.LOGGING_DIRECTORY_PREFIX, ifupdown2_log_dirs[index][0], ifupdown2_log_dirs[index][1])
206 shutil.rmtree(directory_to_remove, ignore_errors=True)
207 except:
208 pass
209
210 @staticmethod
211 def __create_dir(path):
212 if not os.path.isdir(path):
213 os.mkdir(path)
214
215 def set_level(self, default, error=False, warning=False, info=False, debug=False):
216 """
217 Set root handler logging level
218 :param default:
219 :param error:
220 :param warning:
221 :param info:
222 :param debug:
223 """
224 if debug:
225 log_level = logging.DEBUG
226 elif info:
227 log_level = logging.INFO
228 elif warning:
229 log_level = logging.WARNING
230 elif error:
231 log_level = logging.ERROR
232 else:
233 log_level = default
234
235 for handler in self.__root_logger.handlers:
236 if handler == self.__debug_handler:
237 continue
238 handler.setLevel(log_level)
239
240 # make sure that the root logger has the lowest logging level possible
241 # otherwise some messages might not go through
242 if self.__root_logger.level > log_level:
243 self.__root_logger.setLevel(log_level)
244
245 def enable_console(self):
246 """ Add console handler to root logger """
247 self.__root_logger.addHandler(self.__console_handler)
248
249 def disable_console(self):
250 """ Remove console handler from root logger """
251 self.__root_logger.removeHandler(self.__console_handler)
252
253 def enable_syslog(self):
254 """ Add syslog handler to root logger """
255 if self.__syslog_handler and self.__syslog_handler not in self.__root_logger.handlers:
256 self.__root_logger.addHandler(self.__syslog_handler)
257
258 def disable_syslog(self):
259 """ Remove syslog handler from root logger """
260 if self.__syslog_handler:
261 self.__root_logger.removeHandler(self.__syslog_handler)
262
263 def is_syslog_enabled(self):
264 return self.__syslog_handler in self.__root_logger.handlers
265
266 def get_syslog_log_level(self):
267 return self.__syslog_handler.level if self.__syslog_handler else None
268
269 def set_level_syslog(self, level):
270 if self.__syslog_handler:
271 self.__syslog_handler.setLevel(level)
272
273 if self.__root_logger.level > level:
274 self.__root_logger.setLevel(level)
275
276 def close_log_stream(self):
277 """ Close socket to disconnect client.
278 We first have to perform this little hack: it seems like the socket is
279 not opened until data (LogRecord) are transmitted. In our most basic use
280 case (client sends "ifup -a") the daemon doesn't send back any LogRecord
281 but we can't predict that in the client. The client is already in a
282 blocking-select waiting for data on it's socket handler
283 (StreamRequestHandler). For this special case we need to manually call
284 "createSocket" to open the channel to the client so that we can properly
285 close it. That way the client can exit cleanly.
286 """
287 self.__root_logger.removeHandler(self.__socket_handler)
288 self.__socket_handler.acquire()
289 self.__socket_handler.retryTime = None
290 try:
291 if not self.__socket_handler.sock:
292 self.__socket_handler.createSocket()
293 finally:
294 self.__socket_handler.close()
295 self.__socket_handler.release()
296
297 def start_stream(self):
298 self.__root_logger.addHandler(self.__socket_handler)
299
300 def set_daemon_logging_level(self, args):
301 self.set_level(self.DEFAULT_LOGGING_LEVEL_DAEMON, info=args.verbose, debug=args.debug)
302
303 def set_request_logging_level(self, args):
304 if not hasattr(args, "syslog") or not args.syslog:
305 self.disable_syslog()
306 else:
307 self.__root_logger.removeHandler(self.__socket_handler)
308 self.set_level(self.DEFAULT_LOGGING_LEVEL_NORMAL, info=args.verbose, debug=args.debug)
309
310 def start_client_logging(self, args):
311 """ Setup root logger name and client log level
312 syslog is handled by the daemon directly
313 """
314 self.__root_logger.name = self.LOGGER_NAME
315
316 if hasattr(args, "syslog") and args.syslog:
317 self.enable_syslog()
318 self.disable_console()
319
320 self.set_level(self.DEFAULT_LOGGING_LEVEL_NORMAL, info=args.verbose, debug=args.debug)
321
322 def start_standalone_logging(self, args):
323 self.__root_logger.name = self.LOGGER_NAME
324
325 if hasattr(args, "syslog") and args.syslog:
326 self.enable_syslog()
327 self.disable_console()
328
329 self.__root_logger.removeHandler(self.__console_handler)
330
331 self.set_level(self.DEFAULT_LOGGING_LEVEL_NORMAL, info=args.verbose, debug=args.debug)
332
333 def start_daemon_logging(self, args):
334 """
335 Daemon mode initialize a socket handler to transmit logging to the
336 client, we can also do syslog logging and/or console logging (probably
337 just for debugging purpose)
338 :param args:
339 :return:
340 """
341 self.__root_logger.name = self.LOGGER_NAME_DAEMON
342 self.daemon = True
343
344 self.enable_syslog()
345
346 # Create SocketHandler for daemon-client communication
347 self.__socket_handler = logging.handlers.SocketHandler(
348 "localhost",
349 port=self.DEFAULT_TCP_LOGGING_PORT
350 )
351 self.__root_logger.addHandler(self.__socket_handler)
352
353 if not args.console:
354 self.disable_console()
355
356 self.set_daemon_logging_level(args)
357
358 def write(self, msg):
359 root_logger.info(msg)
360
361 def root_logger(self):
362 return self.__root_logger