"""Bcfg2 logging support""" import copy import fcntl import logging import logging.handlers import math import socket import struct import sys import termios import Bcfg2.Options logging.raiseExceptions = 0 class TermiosFormatter(logging.Formatter): """The termios formatter displays output in a terminal-sensitive fashion. """ def __init__(self, fmt=None, datefmt=None): logging.Formatter.__init__(self, fmt, datefmt) if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): # now get termios info try: self.width = struct.unpack('hhhh', fcntl.ioctl(0, termios.TIOCGWINSZ, "\000" * 8))[1] if self.width == 0: self.width = 80 except: # pylint: disable=W0702 self.width = 80 else: # output to a pipe self.width = 32768 def format(self, record): '''format a record for display''' returns = [] line_len = self.width if isinstance(record.msg, str): if len(record.args) != 0: record.msg = record.msg % record.args for line in record.msg.split('\n'): if len(line) <= line_len: returns.append(line) else: inner_lines = \ int(math.floor(float(len(line)) / line_len)) + 1 for msgline in range(inner_lines): returns.append( line[msgline * line_len:(msgline + 1) * line_len]) elif isinstance(record.msg, list): if not record.msg: return '' record.msg.sort() msgwidth = self.width col_width = max([len(item) for item in record.msg]) columns = int(math.floor(float(msgwidth) / (col_width + 2))) lines = int(math.ceil(float(len(record.msg)) / columns)) for lineno in range(lines): indices = [idx for idx in [(colNum * lines) + lineno for colNum in range(columns)] if idx < len(record.msg)] retformat = (len(indices) * (" %%-%ds " % col_width)) returns.append(retformat % tuple([record.msg[idx] for idx in indices])) else: returns.append(str(record.msg)) if record.exc_info: returns.append(self.formatException(record.exc_info)) return '\n'.join(returns) class FragmentingSysLogHandler(logging.handlers.SysLogHandler): """ This handler fragments messages into chunks smaller than 250 characters """ def __init__(self, procname, path, facility): self.procname = procname self.unixsocket = False logging.handlers.SysLogHandler.__init__(self, path, facility) def emit(self, record): """Chunk and deliver records.""" record.name = self.procname if isinstance(record.msg, str): msgs = [] error = record.exc_info record.exc_info = None msgdata = record.msg if len(msgdata) == 0: return while msgdata: newrec = copy.copy(record) newrec.msg = msgdata[:250] msgs.append(newrec) msgdata = msgdata[250:] msgs[0].exc_info = error else: msgs = [record] for newrec in msgs: msg = '<%d>%s\000' % \ (self.encodePriority(self.facility, newrec.levelname.lower()), self.format(newrec)) try: try: encoded = msg.encode('utf-8') except UnicodeDecodeError: encoded = msg self.socket.send(encoded) except socket.error: for i in range(10): # pylint: disable=W0612 try: if isinstance(self.address, tuple): self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.connect(self.address) else: self._connect_unixsocket(self.address) break except socket.error: continue try: reconn = copy.copy(record) reconn.msg = 'Reconnected to syslog' self.socket.send('<%d>%s\000' % (self.encodePriority(self.facility, logging.WARNING), self.format(reconn))) self.socket.send(msg) # If we still fail then drop it. Running # bcfg2-server as non-root can trigger permission # denied exceptions. except: # pylint: disable=W0702 pass def add_console_handler(level=logging.DEBUG): """ Add a logging handler that logs at a level to sys.stderr """ console = logging.StreamHandler(sys.stderr) console.setLevel(level) # tell the handler to use this format console.setFormatter(TermiosFormatter()) try: console.set_name("console") # pylint: disable=E1101 except AttributeError: console.name = "console" # pylint: disable=W0201 logging.root.addHandler(console) def add_syslog_handler(procname=None, syslog_facility='daemon', level=logging.DEBUG): """Add a logging handler that logs as procname to syslog_facility.""" if procname is None: procname = Bcfg2.Options.get_parser().prog try: try: syslog = FragmentingSysLogHandler(procname, '/dev/log', syslog_facility) except socket.error: syslog = FragmentingSysLogHandler(procname, ('localhost', 514), syslog_facility) try: syslog.set_name("syslog") # pylint: disable=E1101 except AttributeError: syslog.name = "syslog" # pylint: disable=W0201 syslog.setLevel(level) syslog.setFormatter( logging.Formatter('%(name)s[%(process)d]: %(message)s')) logging.root.addHandler(syslog) except socket.error: logging.root.error("Failed to activate syslogging") except: print("Failed to activate syslogging") def add_file_handler(level=logging.DEBUG): """Add a logging handler that logs to a file.""" filelog = logging.FileHandler(Bcfg2.Options.setup.logfile) try: filelog.set_name("file") # pylint: disable=E1101 except AttributeError: filelog.name = "file" # pylint: disable=W0201 filelog.setLevel(level) filelog.setFormatter( logging.Formatter('%(asctime)s %(name)s[%(process)d]: %(message)s')) logging.root.addHandler(filelog) def default_log_level(): """ Get the default log level, according to the configuration """ if Bcfg2.Options.setup.debug: return logging.DEBUG elif Bcfg2.Options.setup.verbose: return logging.INFO else: return logging.WARNING def setup_logging(): """Setup logging for Bcfg2 software.""" if hasattr(logging, 'already_setup'): return level = default_log_level() params = [] to_console = True if hasattr(Bcfg2.Options.setup, "daemon"): if Bcfg2.Options.setup.daemon: to_console = False # if a command can be daemonized, but hasn't been, then we # assume that they're running it in the foreground and thus # want some more output. clvl = min(level, logging.INFO) else: clvl = level if to_console: params.append("%s to console" % logging.getLevelName(clvl)) add_console_handler(level=clvl) if hasattr(Bcfg2.Options.setup, "syslog") and Bcfg2.Options.setup.syslog: slvl = min(level, logging.INFO) params.append("%s to syslog" % logging.getLevelName(slvl)) add_syslog_handler(level=slvl) if Bcfg2.Options.setup.logfile: params.append("%s to %s" % (logging.getLevelName(level), Bcfg2.Options.setup.logfile)) add_file_handler(level=level) logging.root.setLevel(logging.DEBUG) logging.root.debug("Configured logging: %s" % "; ".join(params)) logging.already_setup = True class Debuggable(object): """ Mixin to add a debugging interface to an object """ options = [] #: List of names of methods to be exposed as XML-RPC functions, if #: applicable to the child class __rmi__ = ['toggle_debug', 'set_debug'] #: How exposed XML-RPC functions should be dispatched to child #: processes. __child_rmi__ = __rmi__[:] def __init__(self, name=None): """ :param name: The name of the logger object to get. If none is supplied, the full name of the class (including module) will be used. :type name: string """ if name is None: name = "%s.%s" % (self.__class__.__module__, self.__class__.__name__) self.debug_flag = Bcfg2.Options.setup.debug self.logger = logging.getLogger(name) def set_debug(self, debug): """ Explicitly enable or disable debugging. :returns: bool - The new value of the debug flag """ self.debug_flag = debug return debug def toggle_debug(self): """ Turn debugging output on or off. :returns: bool - The new value of the debug flag """ return self.set_debug(not self.debug_flag) def debug_log(self, message, flag=None): """ Log a message at the debug level. :param message: The message to log :type message: string :param flag: Override the current debug flag with this value :type flag: bool :returns: None """ if (flag is None and self.debug_flag) or flag: self.logger.error(message) class _OptionContainer(object): """ Container for options loaded at import-time to configure logging """ options = [ Bcfg2.Options.BooleanOption( '-d', '--debug', help='Enable debugging output', cf=('logging', 'debug')), Bcfg2.Options.BooleanOption( '-v', '--verbose', help='Enable verbose output', cf=('logging', 'verbose')), Bcfg2.Options.PathOption( '-o', '--logfile', help='Set path of file log', cf=('logging', 'path'))] @staticmethod def options_parsed_hook(): """ initialize settings from /etc/bcfg2-web.conf or /etc/bcfg2.conf, or set up basic defaults. this lets manage.py work in all cases """ setup_logging() Bcfg2.Options.get_parser().add_component(_OptionContainer)