usr/src/lib/install_logging_pymod/logger.py
author Ginnie Wray<virginia.wray@oracle.com>
Tue, 19 Jun 2012 12:18:37 -0600
changeset 1717 10cb4d15a248
parent 1529 e73fd49aeb3a
permissions -rw-r--r--
7066254 Problem with install/logging 7170155 DC leaves log files in /var/tmp/install

#!/usr/bin/python
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
# Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
#
''' Code specific for the implementation of the InstallLogger'''
import errno
import logging
import logging.handlers
import os
import shutil
import socket
import struct
import time

# Global variables
DEFAULTPROGRESSFORMAT = '%(progress)s %(msg)s'
DEFAULTLOGLEVEL = logging.DEBUG
DEFAULTDESTINATION = '/var/tmp/install/dest'
MAX_INT = 100

INSTALL_LOGGER_NAME = "InstallationLogger"


class InstallManager(logging.Manager):
    '''A sub-class of the logging.Manager that exists for the purpose
       of allowing applications to pass in a default log rather than
       using the default log provided by the installLogger class.
    '''
    def getLogger(self, name, log=None, level=None, exclusive_rw=False):
        """
        This getLogger method allows the application to pass in the following
        input:

        - name: Required. The name of the logger

        - log: Optional. If a default log is included, it will be set
          up as the default log location for the logging process.

        - level: Optional. This value may be set for a default log
          file. If it is not set, the logger sets to DEBUG as the default
          value. The format of the level should follow the format of
          the logging module. For example, logging.DEBUG, logging.INFO.

        - exclusive_rw - Optional. Opens the file in a more secure mode. It
          ensures safe log file creation and gives the file restrictive
          permissions. If it is not set, it defaults to False.

        These values are passed to the InstallLogger to set up a custom default
        log file.

        The placeholder code is an adjunct to the python logging module.
        It is used to manage the logging hierarchy. Because this getLogger
        method interfaces with the logging hierarchy, it is necessary to
        comply with that structure.
        """
        logger_name = None
        logging._acquireLock()
        try:
            if name in logging.Logger.manager.loggerDict:
                logger_name = logging.Logger.manager.loggerDict[name]
                if isinstance(logger_name, logging.PlaceHolder):
                    placeholder_for_fixup = logger_name
                    logger_name = \
                        logging._loggerClass(name, default_log=log,
                            level=level, exclusive_rw=exclusive_rw)
                    logger_name.manager = self
                    logging.Logger.manager.loggerDict[name] = logger_name
                    self._fixupChildren(placeholder_for_fixup, logger_name)
                    self._fixupParents(logger_name)
            else:
                logger_name = logging._loggerClass(name, default_log=log,
                    level=level, exclusive_rw=exclusive_rw)
                logger_name.manager = self
                logging.Logger.manager.loggerDict[name] = logger_name
                self._fixupParents(logger_name)
        finally:
            logging._releaseLock()
        return logger_name


class LogInitError(Exception):
    '''Raised if error occurs during initialization of logging'''
    pass


class LoggerError(Exception):
    '''Raised if a fatal error occurs in the logger'''
    pass


class ProgressFilter(logging.Filter):
    '''Filters records to determine if a progress message should
       be logged to a handler.
    '''

    def __init__(self, log_progress=True):
        self._log_progress = log_progress
        logging.Filter.__init__(self, name='')

    def filter(self, record):
        '''Checks to see if a record has a progress attribute or levelno'''
        if self._log_progress:
            return hasattr(record, 'progress') or hasattr(record, 'levelno')
        else:
            return not hasattr(record, 'progress')


class HTTPHandler(logging.handlers.HTTPHandler):
    '''The HTTPHandler super class doesn't have a preamble,
       which is required by the installer interface.
    '''

    def __init__(self, host, url, preamble, method="GET"):
        self._preamble = preamble
        logging.handlers.HTTPHandler.__init__(self, host, url, method="GET")


class FileHandler(logging.FileHandler):
    '''The FileHandler super class doesn't check to make sure a
       directory exists, so the check is done here.
    '''

    def __init__(self, filename, mode='a', encoding=None, delay=0,
        exclusive_rw=False):
        if not os.path.exists(os.path.dirname(filename)):
            os.makedirs(os.path.dirname(filename), mode=0777)

        if exclusive_rw:
            delay = True

        # The mode may be set differently by the consumer, so
        # it should be passed through to the constructor.
        logging.FileHandler.__init__(self, filename, mode=mode,
            encoding=encoding, delay=delay)

        if exclusive_rw:
            fd = os.open(self.baseFilename,
                os.O_CREAT | os.O_EXCL | os.O_RDWR, 0644)
            self.stream = os.fdopen(fd, mode)

    def transfer_log(self, destination, isdir=False):
        ''' transfer_log() - method to move the log from its original location
            to the location specified in the destination variable

            isdir - boolean argument to specify the destination is a directory.
            The log file will be moved from it's current location to the
            destination directory
        '''
        # clear out any records in the stream
        if self.stream:
            self.stream.flush()
            self.stream.close()

        # make sure the directory (or parent directory) exists
        if isdir:
            if not os.path.exists(destination):
                os.makedirs(destination)
        else:
            if not os.path.exists(os.path.split(destination)[0]):
                os.makedirs(os.path.split(destination)[0])

        # if the destination is a directory, copy the log file over.  If not,
        # copy the logfile to the renamed file specified
        if isdir:
            newfile = os.path.join(destination,
                                   os.path.basename(self.baseFilename))
            shutil.copy(self.baseFilename, newfile)
            self.baseFilename = newfile
        else:
            shutil.copy(self.baseFilename, destination)
            self.baseFilename = os.path.abspath(destination)

        # reopen the file with a mode of "a"
        self.mode = "a"
        self.stream = self._open()


class InstallFormatter(logging.Formatter):
    '''Sub-class the Formatter class to handle
       the progress records.
    '''

    def format(self, rec):
        if rec.levelno == MAX_INT:
            if self._fmt != DEFAULTPROGRESSFORMAT:
                self._fmt = 'PROGRESS REPORT: progress percent:' + \
                    '%(progress)s %(msg)s'
            rec.message = rec.getMessage()
            message_string = self._fmt % rec.__dict__
            return message_string
        else:
            try:
                return logging.Formatter(
                    fmt=InstallLogger.INSTALL_FORMAT).format(rec)
            except:
                return "Improper logging format. Log message dropped."


class ProgressLogRecord(logging.LogRecord):
    '''Used to construct the log record for the progress reporting'''

    def __init__(self, msg=None, progress=None, name='Progress', level=MAX_INT,
            pathname=None, lineno=None, args=None, exc_info=None, func=None):
        self.msg = msg
        self.progress = None
        self.levelno = level
        logging.LogRecord.__init__(self, name, level, pathname, lineno,
                 msg, args, exc_info, func=func)

        if InstallLogger.ENGINE is None:
            raise LoggerError("No engine is defined for logging")

        if progress >= 0:
            self.progress =  \
                InstallLogger.ENGINE.normalize_progress(progress)


class ProgressHandler(logging.handlers.SocketHandler):
    '''The ProgressHandler manages records that are sent via the
       report_progress method of the InstallLogger. It sends progress records
       to a progress receiver. This can be a remote socket of an on disk file.
       The ProgressHandler provides its own formatting, which formats the data
       for the progress receiver. The ProgressHandler is instantiated as a
       singleton.
    '''

    def __init__(self, host, port):
        logging.handlers.SocketHandler.__init__(self, host, port)

        self.host = host
        self.port = port
        self.createSocket()

    def send(self, data):
        '''Send a string to the socket. This is modified slightly from the
           logging SocketHandler's send method in order to manage the
           progress records.
        '''
        if not self.sock:
            self.createSocket()

        if self.sock:
            try:
                if hasattr(self.sock, "sendall"):
                    self.sock.sendall(struct.pack('@i', len(data)) + data)
                    # A small timeout in case the socket buffer is full
                    time.sleep(.05)
                else:
                    sentsofar = 0
                    left = len(data)
                    while left > 0:
                        sent = self.sock.send(data[sentsofar:])
                        sentsofar = sentsofar + sent
                        left = left - sent
            except socket.error:
                self.sock.close()
                self.sock = None  # so we can call createSocket next time

    def emit(self, record):
        # Format a record and emit the record to the socket.
        try:
            msg = self.format(record)
            self.send(msg)
        except:
            self.handleError(record)


class InstallLogger(logging.Logger):
    '''Sub-Class for logging in the install environment'''

    # Class variables used with the InstallLogger
    ENGINE = None
    DEFAULTFILEHANDLER = None
    INSTALL_FORMAT = '%(asctime)-25s %(name)-10s ' \
        '%(levelname)-10s %(message)-50s'

    def __init__(self, name, default_log=None, level=None, exclusive_rw=False):
        # If logging level was not provided, choose the desired default one.
        # Use DEFAULTLOGLEVEL for top level logger, while default to
        # logging.NOTSET for sub-loggers. That instructs Python logging to
        # inherit logging level from parent.

        self.default_log_file = None
        self._prog_filter = ProgressFilter(log_progress=True)
        self._no_prog_filter = ProgressFilter(log_progress=False)

        if InstallLogger.DEFAULTFILEHANDLER is not None:
            logging.Logger.__init__(self, name)
            return

        if not InstallLogger.DEFAULTFILEHANDLER and default_log:
            self.default_log_file = default_log

        self.exclusive_rw = exclusive_rw

        if level is None:
            if "." in name:
                level = logging.NOTSET
            else:
                level = DEFAULTLOGLEVEL

        logging.Logger.__init__(self, name, level=level)

        # MAX_INT is the level that is associated with progress
        # reporting. The following commands add MAX_INT to the
        # logger's dictionary of log levels.
        logging.addLevelName(MAX_INT, 'MAX_INT')
        logging.addLevelName('MAX_INT', MAX_INT)

        # Initialize the default log.
        if not InstallLogger.DEFAULTFILEHANDLER and self.default_log_file:
            logdir = os.path.dirname(self.default_log_file)

            if not os.path.exists(logdir):
                try:
                    os.makedirs(logdir)
                except OSError as err:
                    if err.errno != errno.EEXIST:
                        raise

                # Make sure default log file is usable by everyone,
                # even if created by root.
                statbuf = os.stat(logdir)
                if (statbuf.st_mode & 01777) != 01777:
                    os.chmod(logdir, 01777)

            InstallLogger.DEFAULTFILEHANDLER = \
                FileHandler(filename=self.default_log_file, mode='a',
                    exclusive_rw=self.exclusive_rw)
            InstallLogger.DEFAULTFILEHANDLER.setLevel(level)
            InstallLogger.DEFAULTFILEHANDLER.setFormatter(InstallFormatter())
            logging.Logger.addHandler(self, InstallLogger.DEFAULTFILEHANDLER)
            InstallLogger.DEFAULTFILEHANDLER.addFilter(self._prog_filter)

    @property
    def default_log(self):
        '''Returns the name of the default log '''
        return InstallLogger.DEFAULTFILEHANDLER.baseFilename

    @property
    def name(self):
        '''returns the name of the logger'''
        return self.name

    @property
    def default_fh(self):
        '''returns the default FileHandler for the logging process'''
        return InstallLogger.DEFAULTFILEHANDLER

    def addHandler(self, handler):
        '''Adds the requested handler to the InstallLogger
           Performs special handling if it is a progress handler
        '''
        logging.Logger.addHandler(self, handler)

        if isinstance(handler, ProgressHandler):
            handler.addFilter(self._prog_filter)
            handler.setLevel(MAX_INT)
            handler.setFormatter(InstallFormatter(fmt='%(progress)s %(msg)s'))
        else:
            handler.addFilter(self._no_prog_filter)
            if handler.formatter is None:
                handler.setFormatter(InstallFormatter())

    def report_progress(self, msg=None, progress=None):
        '''Logs progress reports to the progress receiver'''
        assert(0 <= progress <= 100)
        prog_record = ProgressLogRecord(msg, progress)
        self.handle(prog_record)

    def transfer_log(self, destination=DEFAULTDESTINATION):
        '''
        Requests a transfer of the default log to a requested location,
        currently just another location on disk. It also adds the log
        location to a log file list that is available once logging completes.
        '''
        isDir = os.path.isdir(destination)
        InstallLogger.DEFAULTFILEHANDLER.transfer_log(destination, \
                                                      isDir)

    def close(self):
        '''Terminates logging and provides a list of log files'''

        close_log_list = []
        # Collect the location of log files
        for val in logging.Logger.manager.loggerDict.values():
            if hasattr(val, 'handlers'):
                for handler in val.handlers:
                    if isinstance(handler, FileHandler) and \
                        handler.baseFilename not in close_log_list:
                        close_log_list.append(handler.baseFilename)

        logging.shutdown()
        return close_log_list


# Create an instance of the InstallManager. This is a sub-class of
# the logging module manager. We want to use the same hierarchy
# for this manager, so instantiate it with the root logger from
# the logging module.
InstallLogger.manager = InstallManager(logging.Logger.root)