1 # Copyright (C) 2016, 2017, 2018, 2019 Cumulus Networks, Inc. all rights reserved
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.
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.
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
17 # https://www.gnu.org/licenses/gpl-2.0-standalone.html
20 # Julien Fortin, julien@cumulusnetworks.com
29 import logging
.handlers
31 from datetime
import date
, datetime
34 from ifupdown2
.ifupdown
.utils
import utils
36 from ifupdown
.utils
import utils
39 root_logger
= logging
.getLogger()
43 LOGGER_NAME
= "ifupdown2"
44 LOGGER_NAME_DAEMON
= "ifupdown2d"
46 LOGGING_DIRECTORY
= "/etc/network/ifupdown2/log"
47 LOGGING_DIRECTORY_PREFIX
= "network_config_ifupdown2_"
48 LOGGING_DIRECTORY_LIMIT
= 42
50 DEFAULT_TCP_LOGGING_PORT
= 42422
51 DEFAULT_LOGGING_LEVEL_DAEMON
= logging
.INFO
52 DEFAULT_LOGGING_LEVEL_NORMAL
= logging
.WARNING
58 if not LogManager
.__instance
:
60 LogManager
.__instance
= LogManager()
61 except Exception as e
:
62 sys
.stderr
.write("warning: ifupdown2.Log: %s\n" % str(e
))
64 return LogManager
.__instance
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"
72 if LogManager
.__instance
:
73 raise RuntimeError("Log: invalid access. Please use Log.getInstance()")
75 LogManager
.__instance
= self
77 self
.__fmt
= "%(levelname)s: %(message)s"
79 self
.__debug
_fmt
= "%(asctime)s: %(threadName)s: %(name)s: " \
80 "%(filename)s:%(lineno)d:%(funcName)s(): " \
81 "%(levelname)s: %(message)s"
83 self
.__root
_logger
= logging
.getLogger()
84 self
.__root
_logger
.name
= self
.LOGGER_NAME
86 self
.__debug
_handler
= None
87 self
.__socket
_handler
= None
88 self
.__syslog
_handler
= None
89 self
.__console
_handler
= None
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
)
99 self
.__root
_logger
.addHandler(self
.__console
_handler
)
101 if os
.path
.exists("/dev/log"):
103 self
.__syslog
_handler
= logging
.handlers
.SysLogHandler(
105 facility
=logging
.handlers
.SysLogHandler
.LOG_DAEMON
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
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")
119 self
.__init
_debug
_logging
()
120 except Exception as e
:
121 self
.__root
_logger
.debug("couldn't initialize persistent debug logging: %s" % str(e
))
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.
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]
135 # get the integer amount
136 return int(user_config_limit_str
)
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
,
143 }.get(utils
.get_boolean_from_string(user_config_limit_str
))
146 user_config_limit
= self
.LOGGING_DIRECTORY_LIMIT
148 return user_config_limit
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
()
154 if not user_config_limit
:
155 # user has disabled the feature
158 # create logging directory
159 self
.__create
_dir
(self
.LOGGING_DIRECTORY
)
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
)
165 ifupdown2_log_dirs
.sort(key
=lambda x
: int(x
[0]))
167 # get the last log id
168 if ifupdown2_log_dirs
:
169 last_id
= int(ifupdown2_log_dirs
[-1][0])
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
,
179 "%s_%s" % (date
.today().strftime("%b-%d-%Y"), str(datetime
.now()).split(" ", 1)[1])
181 self
.__create
_dir
(new_dir_path
)
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
)
188 self
.__root
_logger
.addHandler(self
.__debug
_handler
)
189 self
.__root
_logger
.setLevel(logging
.DEBUG
)
191 self
.__root
_logger
.debug("persistent debugging is initialized")
193 # cp ENI and ENI.d in the log directory
194 shutil
.copy2("/etc/network/interfaces", new_dir_path
)
196 shutil
.copytree("/etc/network/interfaces.d/", new_dir_path
)
197 except FileNotFoundError
:
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)
208 def __create_dir(path
):
209 if not os
.path
.isdir(path
):
210 os
.mkdir(path
, mode
=0o400)
212 def set_level(self
, default
, error
=False, warning
=False, info
=False, debug
=False):
214 Set root handler logging level
222 log_level
= logging
.DEBUG
224 log_level
= logging
.INFO
226 log_level
= logging
.WARNING
228 log_level
= logging
.ERROR
232 for handler
in self
.__root
_logger
.handlers
:
233 if handler
== self
.__debug
_handler
:
235 handler
.setLevel(log_level
)
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
)
242 def enable_console(self
):
243 """ Add console handler to root logger """
244 self
.__root
_logger
.addHandler(self
.__console
_handler
)
246 def disable_console(self
):
247 """ Remove console handler from root logger """
248 self
.__root
_logger
.removeHandler(self
.__console
_handler
)
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
)
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
)
260 def is_syslog_enabled(self
):
261 return self
.__syslog
_handler
in self
.__root
_logger
.handlers
263 def get_syslog_log_level(self
):
264 return self
.__syslog
_handler
.level
if self
.__syslog
_handler
else None
266 def set_level_syslog(self
, level
):
267 if self
.__syslog
_handler
:
268 self
.__syslog
_handler
.setLevel(level
)
270 if self
.__root
_logger
.level
> level
:
271 self
.__root
_logger
.setLevel(level
)
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.
284 self
.__root
_logger
.removeHandler(self
.__socket
_handler
)
285 self
.__socket
_handler
.acquire()
286 self
.__socket
_handler
.retryTime
= None
288 if not self
.__socket
_handler
.sock
:
289 self
.__socket
_handler
.createSocket()
291 self
.__socket
_handler
.close()
292 self
.__socket
_handler
.release()
294 def start_stream(self
):
295 self
.__root
_logger
.addHandler(self
.__socket
_handler
)
297 def set_daemon_logging_level(self
, args
):
298 self
.set_level(self
.DEFAULT_LOGGING_LEVEL_DAEMON
, info
=args
.verbose
, debug
=args
.debug
)
300 def set_request_logging_level(self
, args
):
301 if not hasattr(args
, "syslog") or not args
.syslog
:
302 self
.disable_syslog()
304 self
.__root
_logger
.removeHandler(self
.__socket
_handler
)
305 self
.set_level(self
.DEFAULT_LOGGING_LEVEL_NORMAL
, info
=args
.verbose
, debug
=args
.debug
)
307 def start_client_logging(self
, args
):
308 """ Setup root logger name and client log level
309 syslog is handled by the daemon directly
311 self
.__root
_logger
.name
= self
.LOGGER_NAME
313 if hasattr(args
, "syslog") and args
.syslog
:
315 self
.disable_console()
317 self
.set_level(self
.DEFAULT_LOGGING_LEVEL_NORMAL
, info
=args
.verbose
, debug
=args
.debug
)
319 def start_standalone_logging(self
, args
):
320 self
.__root
_logger
.name
= self
.LOGGER_NAME
322 if hasattr(args
, "syslog") and args
.syslog
:
324 self
.disable_console()
326 self
.__root
_logger
.removeHandler(self
.__console
_handler
)
328 self
.set_level(self
.DEFAULT_LOGGING_LEVEL_NORMAL
, info
=args
.verbose
, debug
=args
.debug
)
330 def start_daemon_logging(self
, args
):
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)
338 self
.__root
_logger
.name
= self
.LOGGER_NAME_DAEMON
343 # Create SocketHandler for daemon-client communication
344 self
.__socket
_handler
= logging
.handlers
.SocketHandler(
346 port
=self
.DEFAULT_TCP_LOGGING_PORT
348 self
.__root
_logger
.addHandler(self
.__socket
_handler
)
351 self
.disable_console()
353 self
.set_daemon_logging_level(args
)
355 def write(self
, msg
):
356 root_logger
.info(msg
)
358 def root_logger(self
):
359 return self
.__root
_logger