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
= "/var/log/ifupdown2"
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/", "%s/interfaces.d" % new_dir_path
)
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
):
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)
211 def __create_dir(path
):
212 if not os
.path
.isdir(path
):
215 def set_level(self
, default
, error
=False, warning
=False, info
=False, debug
=False):
217 Set root handler logging level
225 log_level
= logging
.DEBUG
227 log_level
= logging
.INFO
229 log_level
= logging
.WARNING
231 log_level
= logging
.ERROR
235 for handler
in self
.__root
_logger
.handlers
:
236 if handler
== self
.__debug
_handler
:
238 handler
.setLevel(log_level
)
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
)
245 def enable_console(self
):
246 """ Add console handler to root logger """
247 self
.__root
_logger
.addHandler(self
.__console
_handler
)
249 def disable_console(self
):
250 """ Remove console handler from root logger """
251 self
.__root
_logger
.removeHandler(self
.__console
_handler
)
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
)
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
)
263 def is_syslog_enabled(self
):
264 return self
.__syslog
_handler
in self
.__root
_logger
.handlers
266 def get_syslog_log_level(self
):
267 return self
.__syslog
_handler
.level
if self
.__syslog
_handler
else None
269 def set_level_syslog(self
, level
):
270 if self
.__syslog
_handler
:
271 self
.__syslog
_handler
.setLevel(level
)
273 if self
.__root
_logger
.level
> level
:
274 self
.__root
_logger
.setLevel(level
)
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.
287 self
.__root
_logger
.removeHandler(self
.__socket
_handler
)
288 self
.__socket
_handler
.acquire()
289 self
.__socket
_handler
.retryTime
= None
291 if not self
.__socket
_handler
.sock
:
292 self
.__socket
_handler
.createSocket()
294 self
.__socket
_handler
.close()
295 self
.__socket
_handler
.release()
297 def start_stream(self
):
298 self
.__root
_logger
.addHandler(self
.__socket
_handler
)
300 def set_daemon_logging_level(self
, args
):
301 self
.set_level(self
.DEFAULT_LOGGING_LEVEL_DAEMON
, info
=args
.verbose
, debug
=args
.debug
)
303 def set_request_logging_level(self
, args
):
304 if not hasattr(args
, "syslog") or not args
.syslog
:
305 self
.disable_syslog()
307 self
.__root
_logger
.removeHandler(self
.__socket
_handler
)
308 self
.set_level(self
.DEFAULT_LOGGING_LEVEL_NORMAL
, info
=args
.verbose
, debug
=args
.debug
)
310 def start_client_logging(self
, args
):
311 """ Setup root logger name and client log level
312 syslog is handled by the daemon directly
314 self
.__root
_logger
.name
= self
.LOGGER_NAME
316 if hasattr(args
, "syslog") and args
.syslog
:
318 self
.disable_console()
320 self
.set_level(self
.DEFAULT_LOGGING_LEVEL_NORMAL
, info
=args
.verbose
, debug
=args
.debug
)
322 def start_standalone_logging(self
, args
):
323 self
.__root
_logger
.name
= self
.LOGGER_NAME
325 if hasattr(args
, "syslog") and args
.syslog
:
327 self
.disable_console()
329 self
.__root
_logger
.removeHandler(self
.__console
_handler
)
331 self
.set_level(self
.DEFAULT_LOGGING_LEVEL_NORMAL
, info
=args
.verbose
, debug
=args
.debug
)
333 def start_daemon_logging(self
, args
):
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)
341 self
.__root
_logger
.name
= self
.LOGGER_NAME_DAEMON
346 # Create SocketHandler for daemon-client communication
347 self
.__socket
_handler
= logging
.handlers
.SocketHandler(
349 port
=self
.DEFAULT_TCP_LOGGING_PORT
351 self
.__root
_logger
.addHandler(self
.__socket
_handler
)
354 self
.disable_console()
356 self
.set_daemon_logging_level(args
)
358 def write(self
, msg
):
359 root_logger
.info(msg
)
361 def root_logger(self
):
362 return self
.__root
_logger