From: Frank Brehm Date: Tue, 12 Jul 2011 21:18:24 +0000 (+0000) Subject: Größerer Umbau X-Git-Url: https://git.uhu-banane.de/?a=commitdiff_plain;h=10beda6f16c0803bdd8e8d5e2f35a4ba3d42f9f5;p=my-stuff%2Fpy-logrotate.git Größerer Umbau git-svn-id: http://svn.brehm-online.com/svn/my-stuff/python/PyLogrotate/trunk@285 ec8d2aa5-1599-4edb-8739-2b3a1bc399aa --- diff --git a/LogRotate/LogRotateCommon.py b/LogRotate/LogRotateCommon.py new file mode 100755 index 0000000..af03d1b --- /dev/null +++ b/LogRotate/LogRotateCommon.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# $Id$ +# $URL$ + +''' +@author: Frank Brehm +@contact: frank@brehm-online.com +@license: GPL3 +@copyright: (c) 2010-2011 by Frank Brehm, Berlin +@version: 0.1.0 +@summary: Module for common used functions +''' + +import re +import sys +import locale +import logging +import gettext +import csv +import pprint +import email.utils + +revision = '$Revision$' +revision = re.sub( r'\$', '', revision ) +revision = re.sub( r'Revision: ', r'r', revision ) +revision = re.sub( r'\s*$', '', revision ) + +__author__ = 'Frank Brehm' +__copyright__ = '(C) 2011 by Frank Brehm, Berlin' +__contact__ = 'frank@brehm-online.com' +__version__ = '0.1.0 ' + revision +__license__ = 'GPL3' + + +logger = logging.getLogger('pylogrotate.common') +locale_dir = None + +#======================================================================== + +def split_parts( text, keep_quotes = False, raise_on_unbalanced = True): + ''' + Split the given text in chunks by whitespaces or + single or double quoted strings. + + @param text: the text to split in chunks + @type text: str + @param keep_quotes: keep quotes of quoted chunks + @type keep_quotes: bool + @param raise_on_unbalanced: raise an exception on + unbalanced quotes + @type raise_on_unbalanced: bool + + @return: list of chunks + @rtype: list + ''' + + chunks = [] + if text is None: + return chunks + + txt = str(text) + last_chunk = '' + + # Big loop to split the text - until it is empty + while txt != '': + + # add chunk, if there is a chunk left and a whitspace + # at the begin of the line + match = re.search(r"\s+", txt) + if ( last_chunk != '' ) and match: + chunks.append(last_chunk) + last_chunk = '' + + # clean the line + txt = txt.strip() + if txt == '': + break + + # search for a single quoted string at the begin of the line + match = re.search(r"^'((?:\\'|[^'])*)'", txt) + if match: + chunk = match.group(1) + chunk = re.sub(r"\\'", "'", chunk) + if keep_quotes: + chunk = "'" + chunk + "'" + last_chunk += chunk + txt = re.sub(r"^'(?:\\'|[^'])*'", "", txt) + continue + + # search for a double quoted string at the begin of the line + match = re.search(r'^"((?:\\"|[^"])*)"', txt) + if match: + chunk = match.group(1) + chunk = re.sub(r'\\"', '"', chunk) + if keep_quotes: + chunk = '"' + chunk + '"' + last_chunk += chunk + txt = re.sub(r'^"(?:\\"|[^"])*"', "", txt) + continue + + # search for unquoted, whitespace delimited text + # at the begin of the line + match = re.search(r'^((?:[^\s\'"]+|\\\'|\\")+)', txt) + if match: + last_chunk += match.group(1) + txt = re.sub(r'^(?:[^\s\'"]+|\\\'|\\")+', "", txt) + continue + + # Only whitespaces left + match = re.search(r'^\s*$', txt) + if match: + break + + # Check for unbalanced quotes + match = re.search(r'^([\'"].*)\s*', txt) + if match: + chunk = match.group(1) + if raise_on_unbalanced: + raise Exception("Unbalanced quotes in »%s«." % ( str(text) ) ) + else: + last_chunk += chunk + continue + + # Here we should not come to ... + raise Exception("Broken split of »%s«: »%s« left" %( str(text), txt)) + + if last_chunk != '': + chunks.append(last_chunk) + + return chunks + +#------------------------------------------------------------------------ + +def email_valid(address): + ''' + Simple Check for E-Mail addresses + + @param address: the mail address to check + @type address: str + + @return: Validity of the given mil address + @rtype: bool + ''' + + if address is None: + return False + + adr = str(address) + if adr is None or adr == '': + return False + + pattern = r'^[a-z0-9._%-+]+@[a-z0-9._%-]+.[a-z]{2,6}$' + if re.search(pattern, adr, re.IGNORECASE) is None: + return False + + return True + +#------------------------------------------------------------------------ + +def human2bytes(value, si_conform = True, use_locale_radix = False, verbose = 0): + ''' + Converts the given human readable byte value (e.g. 5MB, 8.4GiB etc.) + with a prefix into an integer/long value (without a prefix). + It raises a ValueError on invalid values. + + Available prefixes are: + - kB (1000), KB (1024), KiB (1024) + - MB (1000*1000), MiB (1024*1024) + - GB (1000³), GiB (1024³) + - TB (1000^4), TiB (1024^4) + - PB (1000^5), PiB (1024^5) + + @param value: the value to convert + @type value: str + @param si_conform: use factor 1000 instead of 1024 for kB a.s.o. + @type si_conform: bool + @param use_locale_radix: use the locale version of radix instead of the + english decimal dot. + @type use_locale_radix: bool + @param verbose: level of verbosity + @type verbose: int + + @return: amount of bytes + @rtype: long + ''' + + t = gettext.translation('LogRotateCommon', locale_dir, fallback=True) + _ = t.lgettext + + if value is None: + msg = _("Given value is 'None'.") + raise ValueError(msg) + + radix = '.' + if use_locale_radix: + radix = locale.RADIXCHAR + radix = re.escape(radix) + if verbose > 5: + msg = _("using radix '%s'.") % (radix) + logger.debug(msg) + + value_raw = '' + prefix = None + pattern = r'^\s*\+?(\d+(?:' + radix + r'\d*)?)\s*(\S+)?' + match = re.search(pattern, value) + if match is not None: + value_raw = match.group(1) + prefix = match.group(2) + else: + msg = _("Could not determine bytes in '%s'.") % (value) + raise ValueError(msg) + + if use_locale_radix: + value_raw = re.sub(radix, '.', value_raw, 1) + value_float = float(value_raw) + if prefix is None: + prefix = '' + + factor_bin = long(1024) + factor_si = long(1000) + if not si_conform: + factor_si = factor_bin + + factor = long(1) + + if re.search(r'^\s*(?:b(?:yte)?)?\s*$', prefix, re.IGNORECASE): + factor = long(1) + elif re.search(r'^\s*k(?:[bB](?:[Yy][Tt][Ee])?)?\s*$', prefix): + factor = factor_si + elif re.search(r'^\s*Ki?(?:[bB](?:[Yy][Tt][Ee])?)?\s*$', prefix): + factor = factor_bin + elif re.search(r'^\s*M(?:B(?:yte)?)?\s*$', prefix, re.IGNORECASE): + factor = (factor_si * factor_si) + elif re.search(r'^\s*MiB(?:yte)?\s*$', prefix, re.IGNORECASE): + factor = (factor_bin * factor_bin) + elif re.search(r'^\s*G(?:B(?:yte)?)?\s*$', prefix, re.IGNORECASE): + factor = (factor_si * factor_si * factor_si) + elif re.search(r'^\s*GiB(?:yte)?\s*$', prefix, re.IGNORECASE): + factor = (factor_bin * factor_bin * factor_bin) + elif re.search(r'^\s*T(?:B(?:yte)?)?\s*$', prefix, re.IGNORECASE): + factor = (factor_si * factor_si * factor_si * factor_si) + elif re.search(r'^\s*TiB(?:yte)?\s*$', prefix, re.IGNORECASE): + factor = (factor_bin * factor_bin * factor_bin * factor_bin) + elif re.search(r'^\s*P(?:B(?:yte)?)?\s*$', prefix, re.IGNORECASE): + factor = (factor_si * factor_si * factor_si * factor_si * factor_si) + elif re.search(r'^\s*PiB(?:yte)?\s*$', prefix, re.IGNORECASE): + factor = (factor_bin * factor_bin * factor_bin * factor_bin * factor_bin) + else: + msg = _("Couldn't detect prefix '%s'.") % (prefix) + raise ValueError(msg) + + if verbose > 5: + msg = _("Found factor %d.") % (factor) + logger.debug(msg) + + return long(factor * value_float) + +#------------------------------------------------------------------------ + +def period2days(period, use_locale_radix = False, verbose = 0): + ''' + Converts the given string of the form »5d 8h« in an amount of days. + It raises a ValueError on invalid values. + + Special values of period: + - now (returns 0) + - never (returns float('inf')) + + Valid units for periods are: + - »h[ours]« + - »d[ays]« - default, if bare numbers are given + - »w[eeks]« - == 7 days + - »m[onths]« - == 30 days + - »y[ears]« - == 365 days + + @param period: the period to convert + @type period: str + @param use_locale_radix: use the locale version of radix instead of the + english decimal dot. + @type use_locale_radix: bool + @param verbose: level of verbosity + @type verbose: int + + @return: amount of days + @rtype: float + ''' + + t = gettext.translation('LogRotateCommon', locale_dir, fallback=True) + _ = t.lgettext + + if period is None: + msg = _("Given period is 'None'.") + raise ValueError(msg) + + value = str(period).strip().lower() + if period == '': + msg = _("Given period was empty") + raise ValueError(msg) + + if verbose > 4: + msg = _("Called with '%s'.") % (period) + logger.debug(msg) + + if period == 'now': + return float(0) + + # never - returns a positive infinite value + if period == 'never': + return float('inf') + + days = float(0) + radix = '.' + if use_locale_radix: + radix = locale.RADIXCHAR + radix = re.escape(radix) + if verbose > 5: + msg = _("Using radix '%s'.") % (radix) + logger.debug(msg) + + # Search for hours in value + pattern = r'(\d+(?:' + radix + r'\d*)?)\s*h(?:ours?)?' + if verbose > 5: + msg = _("Pattern '%s'.") % (pattern) + logger.debug(msg) + match = re.search(pattern, value, re.IGNORECASE) + if match: + hours_str = match.group(1) + if use_locale_radix: + hours_str = re.sub(radix, '.', hours_str, 1) + hours = float(hours_str) + days += (hours/24) + if verbose > 4: + msg = _("Found %f hours.") % (hours) + logger.debug(msg) + value = re.sub(pattern, '', value, re.IGNORECASE) + if verbose > 5: + msg = _("Rest after hours: '%s'." % (value)) + logger.debug(msg) + + # Search for weeks in value + pattern = r'(\d+(?:' + radix + r'\d*)?)\s*w(?:eeks?)?' + if verbose > 5: + msg = _("Pattern '%s'.") % (pattern) + logger.debug(msg) + match = re.search(pattern, value, re.IGNORECASE) + if match: + weeks_str = match.group(1) + if use_locale_radix: + weeks_str = re.sub(radix, '.', weeks_str, 1) + weeks = float(weeks_str) + days += (weeks*7) + if verbose > 4: + msg = _("Found %f weeks.") % (weeks) + logger.debug(msg) + value = re.sub(pattern, '', value, re.IGNORECASE) + if verbose > 5: + msg = _("Rest after weeks: '%s'." % (value)) + logger.debug(msg) + + # Search for months in value + pattern = r'(\d+(?:' + radix + r'\d*)?)\s*m(?:onths?)?' + if verbose > 5: + msg = _("Pattern '%s'.") % (pattern) + logger.debug(msg) + match = re.search(pattern, value, re.IGNORECASE) + if match: + months_str = match.group(1) + if use_locale_radix: + months_str = re.sub(radix, '.', months_str, 1) + months = float(months_str) + days += (months*30) + if verbose > 4: + msg = _("Found %f months.") % (months) + logger.debug(msg) + value = re.sub(pattern, '', value, re.IGNORECASE) + if verbose > 5: + msg = _("Rest after months: '%s'." % (value)) + logger.debug(msg) + + # Search for years in value + pattern = r'(\d+(?:' + radix + r'\d*)?)\s*y(?:ears?)?' + if verbose > 5: + msg = _("Pattern '%s'.") % (pattern) + logger.debug(msg) + match = re.search(pattern, value, re.IGNORECASE) + if match: + years_str = match.group(1) + if use_locale_radix: + years_str = re.sub(radix, '.', years_str, 1) + years = float(years_str) + days += (years*365) + if verbose > 4: + msg = _("Found %f years.") % (years) + logger.debug(msg) + value = re.sub(pattern, '', value, re.IGNORECASE) + if verbose > 5: + msg = _("Rest after years: '%s'." % (value)) + logger.debug(msg) + + # At last search for days in value + pattern = r'(\d+(?:' + radix + r'\d*)?)\s*(?:d(?:ays?)?)?' + if verbose > 5: + msg = _("Pattern '%s'.") % (pattern) + logger.debug(msg) + match = re.search(pattern, value, re.IGNORECASE) + if match: + days_str = match.group(1) + if use_locale_radix: + days_str = re.sub(radix, '.', days_str, 1) + days_float = float(days_str) + days += days_float + if verbose > 4: + msg = _("Found %f days.") % (days_float) + logger.debug(msg) + value = re.sub(pattern, '', value, re.IGNORECASE) + if verbose > 5: + msg = _("Rest after days: '%s'." % (value)) + logger.debug(msg) + + # warn, if there is a rest + if re.search(r'^\s*$', value) is None: + msg = _("Invalid content for a period: '%s'.") % (value) + logger.warning(msg) + + if verbose > 4: + msg = _("Total %f days found.") % (days) + logger.debug(msg) + + return days + +#------------------------------------------------------------------------ + +def get_address_list(address_str, verbose = 0): + ''' + Retrieves all mail addresses from address_str and give them back + as a list of tuples. + + @param address_str: the string with all mail addresses as a comma + separated list + @type address_str: str + @param verbose: level of verbosity + @type verbose: int + + @return: list of tuples in the form of the return value + of email.utils.parseaddr() + @rtype: list + + ''' + + t = gettext.translation('LogRotateCommon', locale_dir, fallback=True) + _ = t.lgettext + pp = pprint.PrettyPrinter(indent=4) + + addr_list = [] + addresses = [] + + for row in csv.reader([address_str], doublequote=False, skipinitialspace=True): + for address in row: + addr_list.append(address) + + if verbose > 2: + msg = _("Found address entries:") + "\n" + pp.pformat(addr_list) + logger.debug(msg) + + for address in addr_list: + address = re.sub(r',', ' ', address) + address = re.sub(r'\s+', ' ', address) + pair = email.utils.parseaddr(address) + if verbose > 2: + msg = _("Got mail address pair:") + "\n" + pp.pformat(pair) + logger.debug(msg) + if not email_valid(pair[1]): + msg = _("Found invalid mail address '%s'.") % (address) + logger.warning(msg) + continue + addresses.append(pair) + + return addresses + +#------------------------------------------------------------------------ + +def to_unicode_or_bust(obj, encoding='utf-8'): + ''' + Transforms a string, what is not a unicode string, into a unicode string. + All other objects are left untouched. + + @param obj: the object to transform + @type obj: object + @param encoding: the encoding to use to decode the object + defaults to 'utf-8' + @type encoding: str + + @return: the maybe decoded object + @rtype: object + ''' + + if isinstance(obj, basestring): + if not isinstance(obj, unicode): + obj = unicode(obj, encoding) + + return obj + +#======================================================================== + +if __name__ == "__main__": + pass + +#======================================================================== + +# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotate/LogRotateConfig.py b/LogRotate/LogRotateConfig.py new file mode 100755 index 0000000..ba613de --- /dev/null +++ b/LogRotate/LogRotateConfig.py @@ -0,0 +1,2038 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# $Id$ +# $URL$ + +''' +@author: Frank Brehm +@contact: frank@brehm-online.com +@license: GPL3 +@copyright: (c) 2010-2011 by Frank Brehm, Berlin +@version: 0.0.2 +@summary: module the configuration parsing object for Python logrotating +''' + +import re +import sys +import gettext +import pprint +import os +import os.path +import pwd +import grp +import glob +import logging +import email.utils + +from LogRotateCommon import split_parts, email_valid, period2days, human2bytes +from LogRotateCommon import get_address_list +from LogRotateScript import LogRotateScript + +revision = '$Revision$' +revision = re.sub( r'\$', '', revision ) +revision = re.sub( r'Revision: ', r'r', revision ) +revision = re.sub( r'\s*$', '', revision ) + +__author__ = 'Frank Brehm' +__copyright__ = '(C) 2011 by Frank Brehm, Berlin' +__contact__ = 'frank@brehm-online.com' +__version__ = '0.1.2 ' + revision +__license__ = 'GPL3' + + +#======================================================================== +# Module variables + +# @var: dict with all valid taboo pattern types as keys +# and the resulting regex template for the filename as value +pattern_types = { + 'ext': r'%s$', + 'file': r'^%s$', + 'prefix': r'^%s', +} + +script_directives = [ + 'postrotate', + 'prerotate', + 'firstaction', + 'lastaction', +] + +unsupported_options = ( + 'uncompresscmd', + 'error', +) + +options_with_values = ( + 'mail', + 'compresscmd', + 'statusfile', + 'pidfile', + 'compressext', + 'rotate', + 'maxage', + 'mailfrom', + 'smtphost', + 'smtpport', + 'smtptls', + 'smtpuser', + 'smtppasswd', +) + +boolean_options = ( + 'compress', + 'copy', + 'copytruncate', + 'ifempty', + 'missingok', + 'sharedscripts', +) + +integer_options = ( + 'delaycompress', + 'rotate', + 'start', +) + +string_options = ( + 'extension', + 'compresscmd', + 'compressext', + 'compressoptions', +) + +global_options = ( + 'statusfile', + 'pidfile', + 'mailfrom', + 'smtphost', + 'smtpport', + 'smtptls', + 'smtpuser', + 'smtppasswd', +) + +path_options = ( + 'statusfile', + 'pidfile', +) + +valid_periods = { + 'hourly': (1/24), + '2hourly': (1/12), + '4hourly': (1/6), + '6hourly': (1/4), + '12hourly': (1/2), + 'daily': 1, + '2daily': 2, + 'weekly': 7, + 'monthly': 30, + '2monthly': 60, + '4monthly': 120, + '6monthly': 182, + 'yearly': 365, +} + +yes_values = ( + '1', + 'on', + 'y', + 'yes', + 'true', +) + +no_values = ( + '0', + 'off', + 'n', + 'no', + 'false', +) + + +#======================================================================== + +class LogrotateConfigurationError(Exception): + ''' + Base class for exceptions in this module. + ''' + +#======================================================================== + +class LogrotateConfigurationReader(object): + ''' + Class for reading the configuration for Python logrotating + + @author: Frank Brehm + @contact: frank@brehm-online.com + ''' + + #------------------------------------------------------- + def __init__( self, config_file, + verbose = 0, + local_dir = None, + test_mode = False, + ): + ''' + Constructor. + + @param config_file: the configuration file to use + @type config_file: str + @param verbose: verbosity (debug) level + @type verbose: int + @param local_dir: The directory, where the i18n-files (*.mo) + are located. If None, then system default + (/usr/share/locale) is used. + @type local_dir: str or None + @param test_mode: test mode - no write actions are made + @type test_mode: bool + + @return: None + ''' + + self.local_dir = local_dir + ''' + @ivar: The directory, where the i18n-files (*.mo) are located. + @type: str or None + ''' + + self.t = gettext.translation( + 'LogRotateConfig', + local_dir, + fallback = True + ) + ''' + @ivar: a gettext translation object + @type: gettext.translation + ''' + + _ = self.t.lgettext + + self.verbose = verbose + ''' + @ivar: verbosity level (0 - 9) + @type: int + ''' + + self.config_file = config_file + ''' + @ivar: the initial configuration file to use + @type: str + ''' + + self.test_mode = test_mode + ''' + @ivar: test mode - no write actions are made + @type: bool + ''' + + self.logger = logging.getLogger('pylogrotate.config') + ''' + @ivar: logger object + @type: logging.getLogger + ''' + + self.global_option = {} + ''' + @ivar: all global options + @type: dict + ''' + self.global_option['smtphost'] = 'localhost' + + ############################################# + # the rest of instance variables: + + self.search_path = ['/bin', '/usr/bin'] + ''' + @ivar: ordered list with directories, where executables are searched + @type: list + ''' + self._init_search_path() + + self.shred_command = '/usr/bin/shred' + ''' + @ivar: the system command to shred aged rotated logfiles, if wanted + @type: str + ''' + self.check_shred_command() + + self.default = {} + ''' + @ivar: the default values for directives + @type: dict + ''' + self._reset_defaults() + + self.new_log = None + ''' + @ivar: struct with the current log definition + @type: dict or None + ''' + + self.taboo = [] + ''' + @ivar: taboo patterns for including files of whole directories + @type: list + ''' + self.add_taboo(r'\.rpmnew', 'ext'); + self.add_taboo(r'\.rpmorig', 'ext'); + self.add_taboo(r'\.rpmsave', 'ext'); + self.add_taboo(r',v', 'ext'); + self.add_taboo(r'\.swp', 'ext'); + self.add_taboo(r'~', 'ext'); + self.add_taboo(r'\.', 'prefix'); + self.add_taboo(r'\.bak', 'ext'); + self.add_taboo(r'\.old', 'ext'); + self.add_taboo(r'\.rej', 'ext'); + self.add_taboo(r'CVS', 'file'); + self.add_taboo(r'RCS', 'file'); + self.add_taboo(r'\.disabled', 'ext'); + self.add_taboo(r'\.dpkg-old', 'ext'); + self.add_taboo(r'\.dpkg-dist', 'ext'); + self.add_taboo(r'\.dpkg-new', 'ext'); + self.add_taboo(r'\.cfsaved', 'ext'); + self.add_taboo(r'\.ucf-old', 'ext'); + self.add_taboo(r'\.ucf-dist', 'ext'); + self.add_taboo(r'\.ucf-new', 'ext'); + self.add_taboo(r'\.cfsaved', 'ext'); + self.add_taboo(r'\.rhn-cfg-tmp-*', 'ext'); + + self.config_files = {} + ''' + @ivar: dict with all called and included configuration files + to avoid double including + @type: dict + ''' + + self.config_was_read = False + ''' + @ivar: flag whether the configuration file was read. + @type: bool + ''' + + self.config = [] + ''' + @ivar: the configuration, how it was read from cofiguration file(s) + @type: list + ''' + + self.scripts = {} + ''' + @ivar: dict of LogRotateScript objects + with all named scripts found in configuration + @type: dict + ''' + + self.defined_logfiles = {} + ''' + @ivar: all even defined logfiles after globing of file patterns + @type: dict + ''' + + self.logger.debug( _("Logrotate config reader initialised") ) + + #------------------------------------------------------------ + def __str__(self): + ''' + Typecasting function for translating object structure + into a string + + @return: structure as string + @rtype: str + ''' + + pp = pprint.PrettyPrinter(indent=4) + structure = self.as_dict() + return pp.pformat(structure) + + #------------------------------------------------------- + def as_dict(self): + ''' + Transforms the elements of the object into a dict + + @return: structure as dict + @rtype: dict + ''' + + res = { + 'config': self.config, + 'config_file': self.config_file, + 'config_files': self.config_files, + 'config_was_read': self.config_was_read, + 'default': self.default, + 'defined_logfiles': self.defined_logfiles, + 'global_option': self.global_option, + 'logger': self.logger, + 'local_dir': self.local_dir, + 'new_log': self.new_log, + 'search_path': self.search_path, + 'scripts': {}, + 'shred_command': self.shred_command, + 't': self.t, + 'taboo': self.taboo, + 'test_mode': self.test_mode, + 'verbose': self.verbose, + } + + for script_name in self.scripts.keys(): + res['scripts'][script_name] = self.scripts[script_name].as_dict() + + return res + + #------------------------------------------------------------ + def _reset_defaults(self): + ''' + Resetting self.default to the hard coded values + ''' + + _ = self.t.lgettext + + if self.verbose > 3: + self.logger.debug( _("Resetting default values for directives to hard coded values")) + + self.default = {} + + self.default['compress'] = False + self.default['compresscmd'] = 'internal_gzip' + self.default['compressext'] = None + self.default['compressoptions'] = None + self.default['copy'] = False + self.default['copytruncate'] = False + self.default['create'] = { + 'enabled': False, + 'mode': None, + 'owner': None, + 'group': None, + } + self.default['period'] = 7 + self.default['dateext'] = False + self.default['datepattern'] = '%Y-%m-%d' + self.default['delaycompress'] = None + self.default['extension'] = "" + self.default['ifempty'] = True + self.default['mailaddress'] = None + self.default['mailfirst'] = None + self.default['maxage'] = None + self.default['missingok'] = False + self.default['olddir'] = { + 'dirname': '', + 'dateformat': False, + 'enabled': False, + 'mode': None, + 'owner': None, + 'group': None, + } + self.default['rotate'] = 4 + self.default['sharedscripts'] = False + self.default['shred'] = False + self.default['size'] = None + self.default['start'] = 0 + + #------------------------------------------------------------ + def add_taboo(self, pattern, pattern_type = 'file'): + ''' + Add a pattern to the list of taboo patterns self.taboo + Raises a general exception, if pattern_type is invalid + + @param pattern: The patten to append to the taboo list + @type pattern: str + @param pattern_type: The type of the taboo pattern + ('ext', 'file' or 'prefix') + @type pattern_type: str + + @return: None + ''' + + _ = self.t.lgettext + + if not pattern_type in pattern_types: + raise Exception( _("Invalid taboo pattern type '%s' given") % (pattern_type) ) + + pattern = ( pattern_types[pattern_type] % pattern ) + if self.verbose > 3: + self.logger.debug( _("New taboo pattern: '%s'.") % (pattern) ) + + self.taboo.append(pattern) + + #------------------------------------------------------------ + def _init_search_path(self): + ''' + Initialises the internal list of search pathes + + @return: None + ''' + + _ = self.t.lgettext + dir_included = {} + + # Including default path list from environment $PATH + def_path = os.environ['PATH'] + if not def_path: + def_path = '' + sep = os.pathsep + path_list = [] + for item in def_path.split(sep): + if item: + if os.path.isdir(item): + real_dir = os.path.abspath(item) + if not real_dir in dir_included: + path_list.append(real_dir) + dir_included[real_dir] = True + else: + self.logger.debug( _("'%s' is not a directory") % (item)) + + # Including default path list from python + def_path = os.defpath + for item in def_path.split(sep): + if item: + if os.path.isdir(item): + real_dir = os.path.abspath(item) + if not real_dir in dir_included: + path_list.append(real_dir) + dir_included[real_dir] = True + else: + self.logger.debug( _("'%s' is not a directory") % (item)) + + # Including own defined directories + for item in ('/usr/local/bin', '/sbin', '/usr/sbin', '/usr/local/sbin'): + if os.path.isdir(item): + real_dir = os.path.abspath(item) + if not real_dir in dir_included: + path_list.append(real_dir) + dir_included[real_dir] = True + else: + self.logger.debug( _("'%s' is not a directory") % (item)) + + self.search_path = path_list + + #------------------------------------------------------------ + def _get_std_search_path(self, include_current = False): + ''' + Returns a list with all search directories from $PATH and some additionally + directiories. + + @param include_current: include the current working directory + at the end of the list + @type include_current: bool + + @return: list of search directories + @rtype: list + ''' + + #_ = self.t.lgettext + + path_list = self.search_path + if include_current: + item = os.getcwd() + real_dir = os.path.abspath(item) + path_list.append(real_dir) + + return path_list + + #------------------------------------------------------------ + def check_shred_command(self): + ''' + Checks the availability of a check command. Sets self.shred_command to + this system command or to None, if not found (including a warning). + ''' + + _ = self.t.lgettext + path_list = self._get_std_search_path(True) + + cmd = None + found = False + for search_dir in path_list: + if os.path.isdir(search_dir): + cmd = os.path.join(search_dir, 'shred') + if not os.path.isfile(cmd): + continue + if os.access(cmd, os.X_OK): + found = True + break + else: + self.logger.debug( _("Search path '%s' doesn't exists or is not a directory") % (search_dir)) + + if found: + self.logger.debug( _("Shred command found: '%s'") %(cmd) ) + self.shred_command = cmd + return True + else: + self.logger.warning( _("Shred command not found, shred disabled") ) + self.shred_command = None + return False + + #------------------------------------------------------------ + def check_compress_command(self, command): + ''' + Checks the availability of the given compress command. + + 'internal_zip, 'internal_gzip' and 'internal_bzip2' are accepted as + valid compress commands for compressing with the appropriate python modules. + + @param command: command to validate (absolute or relative for + searching in standard search path) + @type command: str + + @return: absolute path of the compress command, 'internal_gzip', + 'internal_bzip2' or None if not found or invalid + @rtype: str or None + ''' + + _ = self.t.lgettext + path_list = self._get_std_search_path(True) + + match = re.search(r'^\s*internal[\-_\s]?zip\s*', command, re.IGNORECASE) + if match: + return 'internal_zip' + + match = re.search(r'^\s*internal[\-_\s]?gzip\s*', command, re.IGNORECASE) + if match: + return 'internal_gzip' + + match = re.search(r'^\s*internal[\-_\s]?bzip2\s*', command, re.IGNORECASE) + if match: + return 'internal_bzip2' + + if os.path.isabs(command): + if os.access(command, os.X_OK): + return os.path.abspath(command) + else: + return None + + cmd = None + found = False + for search_dir in path_list: + if os.path.isdir(search_dir): + cmd = os.path.join(search_dir, command) + if not os.path.isfile(cmd): + continue + if os.access(cmd, os.X_OK): + found = True + break + else: + self.logger.debug( _("Search path '%s' doesn't exists or is not a directory") % (search_dir)) + + if found: + return os.path.abspath(cmd) + else: + return None + + #------------------------------------------------------------ + def get_config(self): + ''' + Returns the configuration, how it was read from configuration file(s) + + @return: configuration + @rtype: dict or None + ''' + + if not self._read_main_configfile(): + return None + + return self.config + + #------------------------------------------------------------ + def get_scripts(self): + ''' + Returns the scriptlist, how it was read from configuration file(s) + + @return: list of scripts + @rtype: list + ''' + + if not self._read_main_configfile(): + return None + + return self.scripts + + #------------------------------------------------------------ + def _read_main_configfile(self): + ''' + Reads the main configuration file (self.config_file). + + @return: success of reading + @rtype: bool + ''' + + _ = self.t.lgettext + + if self.config_was_read: + return True + + if not os.path.exists(self.config_file): + raise LogrotateConfigurationError( _("File '%s' doesn't exists.") % (self.config_file)) + + self.config_file = os.path.abspath(self.config_file) + + if not self._read(self.config_file): + return None + + self.config_was_read = True + return True + + #------------------------------------------------------------ + def _read(self, configfile): + ''' + Reads the configuration from given configuration file and all + included files. + + @param configfile: the configfile to read + @type configfile: str + ''' + + _ = self.t.lgettext + pp = pprint.PrettyPrinter(indent=4) + self.logger.debug( _("Try reading configuration from '%s' ...") % (configfile) ) + + if not os.path.exists(configfile): + raise LogrotateConfigurationError( _("File '%s' doesn't exists.") % (configfile)) + + if not os.path.isfile(configfile): + raise LogrotateConfigurationError( _("'%s' is not a regular file.") % (configfile)) + + self.config_files[configfile] = True + + self.logger.info( _("Reading configuration from '%s' ...") % (configfile) ) + + cfile = None + try: + cfile = open(configfile, 'Ur') + except IOError, e: + raise LogrotateConfigurationError( ( _("Could not read configuration file '%s'") % (configfile) ) + ': ' + str(e)) + lines = cfile.readlines() + cfile.close() + + # defaults for the big loop + linenr = 0 + in_fd = False + in_script = False + in_logfile_list = False + lastrow = '' + newscript = '' + + # inspect every line of configuration file + for line in lines: + + linenr += 1 + line = line.strip() + + # Perform a backslash at the end of the line + line = lastrow + line + match = re.search(r'\\$', line) + if match: + line = re.sub(r'\\$', '', line) + lastrow = line + continue + lastrow = '' + + # delete comments + line = re.sub(r'^#.*', '', line) + if line == '': + continue + + # perform script content + if in_script: + match = re.search(r'^endscript$', line) + if match: + in_script = False + continue + #self.scripts[newscript]['cmd'].append(line) + self.scripts[newscript].add_cmd(line) + continue + + # start of a logfile definition + if line == '{': + + if self.verbose > 3: + self.logger.debug( ( _("Starting a logfile definition (file '%(file)s', line %(line)s)") + % {'file': configfile, 'line': linenr})) + + self._start_logfile_definition( + line = line, + filename = configfile, + in_fd = in_fd, + in_logfile_list = in_logfile_list, + linenr = linenr + ) + in_fd = True + in_logfile_list = False + continue + + # start of a logfile pattern + match = re.search(r'^[\'"]', line) + if match or os.path.isabs(line): + + if in_fd: + raise LogrotateConfigurationError( + ( _("Logfile pattern definition not allowed inside a logfile definition (file '%(file)s', line %(line)s)") + % {'file': configfile, 'line': linenr}) + ) + do_start_logfile_definition = False + + # look, whether a start of a logfile definition is necessary + match_bracket = re.search(r'\s*{\s*$', line) + if match_bracket: + line = re.sub(r'\s*{\s*$', '', line) + do_start_logfile_definition = True + if not in_logfile_list: + self._start_new_log(configfile, linenr) + in_logfile_list = True + + parts = split_parts(line) + if self.verbose > 3: + self.logger.debug( + ( _("Split into parts of: '%s'") % (line)) + + ":\n" + pp.pformat(parts) + ) + + for pattern in parts: + if pattern == '{': + raise LogrotateConfigurationError( + ( _("Syntax error: open curly bracket inside a logfile pattern definition (file '%(file)s', line %(line)s)") + % {'file': configfile, 'line': linenr}) + ) + self.new_log['file_patterns'].append(pattern) + + # start of a logfile definition, if necessary + if do_start_logfile_definition: + self._start_logfile_definition( + line = line, + filename = configfile, + in_fd = in_fd, + in_logfile_list = in_logfile_list, + linenr = linenr + ) + in_fd = True + in_logfile_list = False + + continue + + # end of a logfile definition + match = re.search(r'^}(.*)', line) + if match: + if not in_fd: + raise LogrotateConfigurationError( + ( _("Syntax error: unbalanced closing curly bracket found (file '%(file)s', line %(line)s)") + % {'file': configfile, 'line': linenr}) + ) + rest = match.group(1) + if self.verbose > 2: + self.logger.debug( ( _("End of a logfile definition (file '%(file)s', line %(line)s)") % {'file': configfile, 'line': linenr})) + if rest: + self.logger.warning( + ( _("Needless content found at the end of a logfile definition found: '%(rest)s' (file '%(file)s', line %(line)s)") + % { 'rest': str(rest), 'file': configfile, 'line': linenr}) + ) + # set a compress ext, if Compress is True + if self.new_log['compress']: + if not self.new_log['compressext']: + if self.new_log['compresscmd'] == 'internal_gzip': + self.new_log['compressext'] = '.gz' + elif self.new_log['compresscmd'] == 'internal_zip': + self.new_log['compressext'] = '.zip' + elif self.new_log['compresscmd'] == 'internal_bzip2': + self.new_log['compressext'] = '.bz2' + else: + msg = _("No extension for compressed logfiles given " + + "(File of definition: '%(file)s', start definition: %(rownum)d).") \ + % { 'file': self.new_log['configfile'], 'rownum': self.new_log['configrow']} + raise LogrotateConfigurationError(msg) + # set ifempty => True, if a minsize was given + if self.new_log['size']: + self.new_log['ifempty'] = False + found_files = self._assign_logfiles() + if self.verbose > 3: + self.logger.debug( ( _("New logfile definition:") + "\n" + pp.pformat(self.new_log))) + if found_files > 0: + if self.new_log['postrotate']: + script = self.new_log['postrotate'] + if self.scripts[script]: + self.scripts[script].post_files += found_files + else: + msg = _("Postrotate script '%s' not found.") % (script) + self.logger.error(msg) + if self.new_log['lastaction']: + script = self.new_log['lastaction'] + if self.scripts[script]: + self.scripts[script].last_files += found_files + else: + msg = _("Lastaction script '%s' not found.") % (script) + self.logger.error(msg) + self.config.append(self.new_log) + in_fd = False + in_logfile_list = False + + continue + + # performing includes + match = re.search(r'^include(?:\s+(.*))?$', line, re.IGNORECASE) + if match: + rest = match.group(1) + if in_fd or in_logfile_list: + self.logger.warning( + ( _("Syntax error: include may not appear inside of log file definition (file '%(file)s', line %(line)s)") + % {'file': configfile, 'line': linenr}) + ) + continue + self._do_include(line, rest, configfile, linenr) + continue + + # start of a (regular) script definition + pattern = r'^(' + '|'.join(script_directives) + r')(\s+.*)?$' + match = re.search(pattern, line, re.IGNORECASE) + if match: + script_type = match.group(1).lower() + script_name = None + if match.group(2) is not None: + values = split_parts(match.group(2)) + if values[0]: + script_name = values[0] + if self.verbose > 3: + self.logger.debug( + ( _("Found start of a regular script definition: type: '%(type)s', name: '%(name)s' (file '%(file)s', line %(line)s)") + % {'type': script_type, 'name': script_name, 'file': configfile, 'line': linenr}) + ) + newscript = self._start_log_script_definition( + script_type = script_type, + script_name = script_name, + line = line, + filename = configfile, + in_fd = in_fd, + linenr = linenr, + ) + if newscript: + in_script = True + if self.verbose > 3: + self.logger.debug( ( _("New log script name: '%s'.") % (newscript) )) + continue + + # start of an explicite external script definition + match = re.search(r'^script(\s+.*)?$', line, re.IGNORECASE) + if match: + if self.verbose > 3: + self.logger.debug( ( _("Found start of a external script definition. (file '%(file)s', line %(line)s)") + % {'file': configfile, 'line': linenr}) + ) + rest = match.group(1) + if in_fd or in_logfile_list: + self.logger.warning( + ( _("Syntax error: external script definition may not appear inside of a log file definition (file '%(file)s', line %(line)s)") + % {'file': configfile, 'line': linenr}) + ) + continue + newscript = self._ext_script_definition( + line, rest, configfile, linenr + ) + if newscript: + in_script = True + if self.verbose > 3: + self.logger.debug( ( _("New external script name: '%s'.") % (newscript) )) + continue + + # all other options + if not self._option(line, in_fd, configfile, linenr): + self.logger.warning( ( _("Syntax error in file '%(file)s', line %(line)s") + % {'file': configfile, 'line': linenr}) + ) + + return True + + #------------------------------------------------------------ + def _option(self, line, in_fd, filename, linenr): + ''' + Checks the given line as a logrotate option and assign this + option on success to the default options or in the current + logfile directive + + @param line: line of current config file + @type line: str + @param in_fd: parsing inside a logfile definition + @type in_fd: bool + @param filename: current configuration file + @type filename: str + @param linenr: current line number of configuration file + @type linenr: int + + @return: success of parsing this option + @rtype: bool + ''' + + _ = self.t.lgettext + if self.verbose > 4: + self.logger.debug( + ( _("Checking line '%(line)s' for a logrotate option. (file '%(file)s', line %(lnr)s)") + % {'line': line, 'file': filename, 'lnr': linenr}) + ) + + # where to insert the option? + directive = self.default + directive_str = 'default' + if in_fd: + directive = self.new_log + directive_str = 'new_log' + + # extract option from line + option = None + val = None + match = re.search(r'^(\S+)\s*(.*)', line) + if match: + option = match.group(1).lower() + val = match.group(2) + else: + self.logger.warning( ( _("Could not detect option in line '%s'.") % (line))) + return False + val = re.sub(r'^\s+$', '', val) + if self.verbose > 4: + msg = _("Found option '%(opt)s' with value '%(val)s'.") \ + % {'opt': option, 'val': val} + self.logger.debug(msg) + + # Check for unsupported options + pattern = r'^(' + '|'.join(unsupported_options) + r')$' + match = re.search(pattern, option, re.IGNORECASE) + if match: + self.logger.info( + ( _("Unsupported option '%(option)s'. (file '%(file)s', line %(lnr)s)") + % {'option': match.group(1).lower(), 'file': filename, 'lnr': linenr}) + ) + return True + + # Check for boolean option + pattern = r'^(not?)?(' + '|'.join(boolean_options) + r')$' + match = re.search(pattern, option, re.IGNORECASE) + if match: + negated = match.group(1) + key = match.group(2).lower() + if val: + self.logger.warning( + ( _("Found value '%(value)s' behind the boolean option '%(option)s', ignoring. (file '%(file)s', line %(lnr)s)") + % {'value': val, 'option': option, 'file': filename, 'lnr': linenr}) + ) + if negated is None: + option_value = True + else: + option_value = False + if self.verbose > 4: + self.logger.debug( + ( _("Setting boolean option '%(option)s' in '%(directive)s' to '%(value)s'. (file '%(file)s', line %(lnr)s)") + % {'option': key, 'directive': directive_str, 'value': str(option_value), 'file': filename, 'lnr': linenr}) + ) + directive[key] = option_value + if key == 'copy' and option_value: + if directive['copytruncate']: + msg = _("Option 'copy' disables option 'copytruncate'. (file '%(file)s', line %(lnr)s)") \ + % {'file': filename, 'lnr': linenr} + self.logger.warning(msg) + directive['copytruncate'] = False + if directive['create']['enabled']: + msg = _("Option 'copy' disables option 'create'. (file '%(file)s', line %(lnr)s)") \ + % {'file': filename, 'lnr': linenr} + self.logger.warning(msg) + directive['create']['enabled'] = False + elif key == 'copytruncate' and option_value: + if directive['copy']: + msg = _("Option 'copytruncate' disables option 'copy'. (file '%(file)s', line %(lnr)s)") \ + % {'file': filename, 'lnr': linenr} + self.logger.warning(msg) + directive['copy'] = False + if directive['create']['enabled']: + msg = _("Option 'copytruncate' disables option 'create'. (file '%(file)s', line %(lnr)s)") \ + % {'file': filename, 'lnr': linenr} + self.logger.warning(msg) + directive['create']['enabled'] = False + return True + + # Check for integer options + pattern = r'^(not?)?(' + '|'.join(integer_options) + r')$' + match = re.search(pattern, option, re.IGNORECASE) + if match: + negated = match.group(1) + key = match.group(2).lower() + option_value = 0 + if negated is None: + if key in options_with_values: + if val is None or val == '': + self.logger.warning( ( _("Option '%s' without a necessary value.") % (key))) + return False + else: + if val is None or val == '': + val = '1' + try: + option_value = long(val) + except ValueError, e: + self.logger.warning( + ( _("Option '%(option)s' has no integer value: %(msg)s.") + % {'option': key, 'msg': str(e)}) + ) + return False + if option_value < 0: + self.logger.warning( + ( _("Negative value %(value)s for option '%(option)s' is not allowed.") + % {'value': str(option_value), 'option': key}) + ) + return False + if self.verbose > 4: + self.logger.debug( + ( _("Setting integer option '%(option)s' in '%(directive)s' to '%(value)s'. (file '%(file)s', line %(lnr)s)") + % {'option': key, 'directive': directive_str, 'value': str(option_value), 'file': filename, 'lnr': linenr}) + ) + directive[key] = option_value + return True + + # Check for mail address + match = re.search(r'^(not?)?mail$', option, re.IGNORECASE) + if match: + negated = match.group(1) + if negated: + directive['mailaddress'] = None + if val is not None and val != '': + self.logger.warning( + ( _("Senseless option value '%(value)s' after '%(option)s'.") + % {'value': val, 'option': option.lower()}) + ) + return False + return True + address_list = get_address_list(val, self.verbose) + if len(address_list): + directive['mailaddress'] = address_list + else: + directive['mailaddress'] = None + if self.verbose > 4: + pp = pprint.PrettyPrinter(indent=4) + msg = _("Setting mail address in '%(directive)s' to '%(addr)s'. (file '%(file)s', line %(lnr)s)") \ + % { + 'directive': directive_str, + 'addr': pp.pformat(directive['mailaddress']), + 'file': filename, + 'lnr': linenr, + } + self.logger.debug(msg) + return True + + # Check for mailfirst/maillast + match = re.search(r'^mail(first|last)$', option, re.IGNORECASE) + if match: + when = match.group(1).lower() + option_value = False + if when == 'first': + option_value = True + directive['mailfirst'] = option_value + if self.verbose > 4: + self.logger.debug( + ( _("Setting mailfirst in '%(directive)s' to '%(value)s'. (file '%(file)s', line %(lnr)s)") + % {'directive': directive_str, 'value': str(option_value), 'file': filename, 'lnr': linenr}) + ) + if val is not None and val != '': + self.logger.warning( + ( _("Senseless option value '%(value)s' after '%(option)s'.") + % {'value': val, 'option': option.lower()}) + ) + return False + return True + + # Check for string options + pattern = r'^(' + '|'.join(string_options) + r')$' + match = re.search(pattern, option, re.IGNORECASE) + if match: + key = match.group(1).lower() + if key in options_with_values: + if self.verbose > 5: + self.logger.debug( ( _("Option '%s' must have a value.") %(key))) + if (val is None) or (val == ''): + self.logger.warning( ( _("Option '%s' without a value") %(key))) + return False + if key == 'compresscmd': + prog = self.check_compress_command(val) + if prog is None: + self.logger.warning( ( _("Compress command '%s' not found.") %(val))) + return False + val = prog + if key == 'compressoptions' and val is None: + val = '' + directive[key] = val + return True + + # Check for global options + pattern = r'^(' + '|'.join(global_options) + r')$' + match = re.search(pattern, option, re.IGNORECASE) + if match: + key = match.group(1).lower() + if in_fd: + self.logger.warning( ( _("Option '%s' not allowed inside a logfile directive.") %(key))) + return False + if key in options_with_values: + if self.verbose > 5: + self.logger.debug( ( _("Option '%s' must have a value.") %(key))) + if (val is None) or (re.search(r'^\s*$', val) is not None): + self.logger.warning( ( _("Option '%s' without a value") %(key))) + return False + if key in path_options: + if not os.path.abspath(val): + self.logger.warning( + ( _("Value '%(value)s' for option '%(option)s' is not an absolute path.") + % {'value': val, 'option': key} ) + ) + return False + if key == 'mailfrom': + pair = email.utils.parseaddr(val) + if not email_valid(pair[1]): + msg = _("Invalid mail address for 'mailfrom' given: '%s'.") % (val) + self.logger.warning(msg) + return False + val = pair + elif key == 'smtpport': + port = 25 + try: + port = int(val) + except ValueError, e: + msg = _("Invalid SMTP port '%s' given.") % (val) + self.logger.warning(msg) + return False + if port < 1 or port >= 2**15: + msg = _("Invalid SMTP port '%s' given.") % (val) + self.logger.warning(msg) + return False + val = port + elif key == 'smtptls': + use_tls = False + match = re.search(r'^\s*(?:0+|false|no?)\s*$', val, re.IGNORECASE) + if not match: + match = re.search(r'^\s*(?:1|true|y(?:es)?)\s*$', val, re.IGNORECASE) + if match: + use_tls = True + else: + use_tls = bool(val) + val = use_tls + if self.verbose > 4: + self.logger.debug( + ( _("Setting global option '%(option)s' to '%(value)s'. (file '%(file)s', line %(lnr)s)") + % {'option': key, 'directive': directive_str, 'value': str(val), 'file': filename, 'lnr': linenr}) + ) + self.global_option[key] = val + return True + + # Check for rotation period + pattern = r'^(' + '|'.join(valid_periods.keys()) + r'|period)$' + match = re.search(pattern, option, re.IGNORECASE) + if match: + key = match.group(1).lower() + if self.verbose > 4: + self.logger.debug( + ( _("Checking 'period': key '%(key)s', value '%(value)s'. (file '%(file)s', line %(lnr)s)") + % {'key': key, 'value': str(val), 'file': filename, 'lnr': linenr}) + ) + option_value = 1 + if key in valid_periods: + if (val is not None) and (re.search(r'^\s*$', val) is None): + self.logger.warning( + ( _("Option '%(option)s' may not have a value ('%(value)s'). (file '%(file)s', line %(lnr)s)") + % {'option': key, 'value': val, 'file': filename, 'lnr': linenr}) + ) + option_value = valid_periods[key] + else: + try: + option_value = period2days(val, verbose = self.verbose) + except ValueError, e: + self.logger.warning( ( _("Invalid period definition: '%s'") %(val) )) + return False + if self.verbose > 4: + self.logger.debug( + ( _("Setting 'period' in '%(directive)s' to %(days)f days. (file '%(file)s', line %(lnr)s)") + % {'directive': directive_str, 'days': option_value, 'file': filename, 'lnr': linenr}) + ) + directive['period'] = option_value + return True + + # get maximum age of old rotated log files + match = re.search(r'^(not?)?maxage$', option, re.IGNORECASE) + if match: + negated = False + if match.group(1) is not None: + negated = True + if (val is None) or re.search(r'^\s*$', val) is not None: + negated = True + option_value = 0 + if not negated: + try: + option_value = period2days(val, verbose = self.verbose) + except ValueError, e: + self.logger.warning( ( _("Invalid maxage definition: '%s'") %(val) )) + return False + if self.verbose > 4: + self.logger.debug( + ( _("Setting 'maxage' in '%(directive)s' to %(days)f days. (file '%(file)s', line %(lnr)s)") + % {'directive': directive_str, 'days': option_value, 'file': filename, 'lnr': linenr}) + ) + directive['maxage'] = option_value + return True + + # Setting date extension of rotated log files + match = re.search(r'^(no)?dateext$', option, re.IGNORECASE) + if match: + + negated = False + if match.group(1) is not None: + negated = True + use_dateext = False + dateext = None + + if self.verbose > 4: + self.logger.debug( + ( _("Checking 'dateext', negated: '%(negated)s'. (file '%(file)s', line %(lnr)s)") + % {'negated': str(negated), 'file': filename, 'lnr': linenr}) + ) + values = [] + if val is not None: + values = split_parts(val) + + if not negated: + first_val = '' + if len(values) > 0: + first_val = values[0].lower() + option_value = first_val + if first_val is None or \ + re.search(r'^\s*$', first_val) is not None: + option_value = 'true' + if self.verbose > 5: + self.logger.debug( + ( _("'dateext': first_val: '%(first_val)s', option_value: '%(value)s'. (file '%(file)s', line %(lnr)s)") + % {'first_val': first_val, 'value': option_value, 'file': filename, 'lnr': linenr}) + ) + if option_value in yes_values: + use_dateext = True + elif option_value in no_values: + use_dateext = False + else: + use_dateext = True + dateext = val + + if self.verbose > 4: + self.logger.debug( + ( _("Setting 'dateext' in '%(directive)s' to %(ext)s. (file '%(file)s', line %(lnr)s)") + % {'directive': directive_str, 'ext': str(use_dateext), 'file': filename, 'lnr': linenr}) + ) + directive['dateext'] = use_dateext + + if dateext is not None: + if self.verbose > 4: + self.logger.debug( + ( _("Setting 'datepattern' in '%(directive)s' to '%(pattern)s'. (file '%(file)s', line %(lnr)s)") + % {'directive': directive_str, 'pattern': dateext, 'file': filename, 'lnr': linenr}) + ) + directive['datepattern'] = dateext + + return True + + # Checking for create options ... + match = re.search(r'(not?)?create$', option, re.IGNORECASE) + if match: + + negated = False + if match.group(1) is not None: + negated = True + + if self.verbose > 5: + self.logger.debug( + ( _("Checking for 'create' ... (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + + if negated: + if self.verbose > 4: + self.logger.debug( + ( _("Removing 'create'. (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + directive['create']['enabled'] = False + return True + + if directive['copy']: + msg = _("Option 'copy' was set, so option 'create' has no effect. (file '%(file)s', line %(lnr)s)") \ + % {'file': filename, 'lnr': linenr} + self.logger.warning(msg) + directive['create']['enabled'] = False + return True + + if directive['copytruncate']: + msg = _("Option 'copytruncate' was set, so option 'create' has no effect. (file '%(file)s', line %(lnr)s)") \ + % {'file': filename, 'lnr': linenr} + self.logger.warning(msg) + directive['create']['enabled'] = False + return True + + values = [] + if val is not None: + values = split_parts(val) + + directive['create']['enabled'] = True + + mode = None + owner = None + group = None + + # Check for create mode + if len(values) > 0: + if self.verbose > 5: + self.logger.debug( + ( _("Trying to determine create mode '%(mode)s'... (file '%(file)s', line %(lnr)s)") + % {'mode': values[0], 'file': filename, 'lnr': linenr}) + ) + mode_octal = values[0] + if re.search(r'^0', mode_octal) is None: + mode_octal = '0' + mode_octal + try: + mode = int(mode_octal, 8) + except ValueError: + self.logger.warning( ( _("Invalid create mode '%s'.") %(values[1]))) + return False + + # Check for Owner (user, uid) + if len(values) > 1: + owner_raw = values[1] + if self.verbose > 5: + self.logger.debug( + ( _("Trying to determine create owner '%(owner)s'... (file '%(file)s', line %(lnr)s)") + % {'owner': owner_raw, 'file': filename, 'lnr': linenr}) + ) + if re.search(r'^[1-9]\d*$', owner_raw) is not None: + owner = int(owner_raw) + else: + try: + owner = pwd.getpwnam(owner_raw)[2] + except KeyError: + self.logger.warning( ( _("Invalid owner '%s' in 'create'.") %(owner_raw))) + return False + + # Check for Group (gid) + if len(values) > 2: + group_raw = values[2] + if self.verbose > 5: + self.logger.debug( + ( _("Trying to determine create group '%(group)s'... (file '%(file)s', line %(lnr)s)") + % {'group': group_raw, 'file': filename, 'lnr': linenr}) + ) + if re.search(r'^[1-9]\d*$', group_raw) is not None: + group = int(group_raw) + else: + try: + group = grp.getgrnam(group_raw)[2] + except KeyError: + self.logger.warning( ( _("Invalid group '%s' in 'create'.") %(group_raw))) + return False + + # Give values back ... + directive['create']['mode'] = mode + directive['create']['owner'] = owner + directive['create']['group'] = group + return True + + # checking for olddir ... + match = re.search(r'^(not?)?olddir$', option, re.IGNORECASE) + if match: + + negated = False + if match.group(1) is not None: + negated = True + + if self.verbose > 5: + self.logger.debug( + ( _("Checking for 'olddir' ... (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + + if negated: + if self.verbose > 4: + self.logger.debug( + ( _("Removing 'olddir'. (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + directive['olddir']['enabled'] = False + return True + + values = [] + if val is not None: + values = split_parts(val) + + # Check for dirname of olddir + if len(values) < 1 or values[0] is None or re.search(r'^\s*$', values[0]) is not None: + self.logger.warning( ( _("Option 'olddir' without a value given."))) + return False + directive['olddir']['dirname'] = values[0] + directive['olddir']['enabled'] = True + + mode = None + owner = None + group = None + + # Check for create mode of olddir + if len(values) > 1: + if self.verbose > 5: + self.logger.debug( + ( _("Trying to determine olddir create mode '%(mode)s' ... (file '%(file)s', line %(lnr)s)") + % {'mode': values[1], 'file': filename, 'lnr': linenr}) + ) + mode_octal = values[1] + if re.search(r'^0', mode_octal) is None: + mode_octal = '0' + mode_octal + try: + mode = int(mode_octal, 8) + except ValueError: + self.logger.warning( ( _("Invalid create mode '%s' in 'olddir'.") %(values[1]))) + return False + + # Check for Owner (user, uid) + if len(values) > 2: + owner_raw = values[2] + if self.verbose > 5: + self.logger.debug( + ( _("Trying to determine olddir owner '%(owner)s' ... (file '%(file)s', line %(lnr)s)") + % {'owner': owner_raw, 'file': filename, 'lnr': linenr}) + ) + if re.search(r'^[1-9]\d*$', owner_raw) is not None: + owner = int(owner_raw) + else: + try: + owner = pwd.getpwnam(owner_raw)[2] + except KeyError: + self.logger.warning( ( _("Invalid owner '%s' in 'olddir'.") %(owner_raw))) + return False + + # Check for Group (gid) + if len(values) > 3: + group_raw = values[3] + if self.verbose > 5: + self.logger.debug( + ( _("Trying to determine olddir group '%(group)s' ... (file '%(file)s', line %(lnr)s)") + % {'group': group_raw, 'file': filename, 'lnr': linenr}) + ) + if re.search(r'^[1-9]\d*$', group_raw) is not None: + group = int(group_raw) + else: + try: + group = grp.getgrnam(group_raw)[2] + except KeyError: + self.logger.warning( ( _("Invalid group '%s' in 'olddir'.") %(group_raw))) + return False + + # Give values back ... + directive['olddir']['mode'] = mode + directive['olddir']['owner'] = owner + directive['olddir']['group'] = group + return True + + # Check for minimum size for ratation + match = re.search(r'^size(?:(?:\s*=|\s)|$)', line, re.IGNORECASE) + if match: + size_str = re.sub(r'^size(?:\s*=\s*|\s+)', '', line) + if self.verbose > 5: + self.logger.debug( + ( _("Checking for option 'size', value: '%(value)s' ... (file '%(file)s', line %(lnr)s)") + % {'value': size_str, 'file': filename, 'lnr': linenr}) + ) + if size_str is None: + self.logger.warning( _("Failing size definition.")) + return False + size_bytes = None + try: + size_bytes = human2bytes(size_str, verbose = self.verbose) + except ValueError, e: + self.logger.warning( ( _("Invalid definition for 'size': '%s'.") %(size_str))) + return False + if self.verbose > 4: + self.logger.debug( + ( _("Got a rotation size in '%(directive)s' of %(bytes)d bytes. (file '%(file)s', line %(lnr)s)") + % {'directive': directive_str, 'bytes': size_bytes, 'file': filename, 'lnr': linenr}) + ) + directive['size'] = size_bytes + return True + + # Check for taboo options + pattern = r'^taboo(ext|file|prefix)$' + match = re.search(pattern, option, re.IGNORECASE) + if match: + key = match.group(1).lower() + if self.verbose > 5: + self.logger.debug( + ( _("Checking for option 'taboo%(type)s', value: '%(value)s' ... (file '%(file)s', line %(lnr)s)") + % {'type': key, 'value': val, 'file': filename, 'lnr': linenr}) + ) + + if in_fd: + self.logger.warning( ( _("Option 'taboo%s' not allowed inside a logfile directive.") %(key))) + return False + + values = [] + if val is not None: + values = split_parts(val) + + extend = False + if len(values) > 0 and values[0] is not None and values[0] == '+': + extend = True + values.pop(0) + + if len(values) < 1: + self.logger.warning( ( _("Option 'taboo%s' needs a value.") %(key))) + return False + + if not extend: + self.taboo = [] + for extension in values: + self.add_taboo(extension, key) + + return True + + # Option not found, I'm angry + self.logger.warning( ( _("Unknown option '%s'.") %(option))) + return False + + #------------------------------------------------------------ + def _ext_script_definition(self, line, rest, filename, linenr): + ''' + Starts a new explicite external script definition. + It raises a LogrotateConfigurationError on error. + + @param line: line of current config file + @type line: str + @param rest: rest of the current line after »script« + @type rest: str + @param filename: current configuration file + @type filename: str + @param linenr: current line number of configuration file + @type linenr: int + + @return: name of the script (if a new script definition) or None + @rtype: str or None + ''' + + _ = self.t.lgettext + + # split the rest in chunks + values = split_parts(rest) + + # insufficient arguments to include ... + if len(values) < 1: + self.logger.warning( + ( _("No script name given in a script directive. (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + return None + + # to much arguments to include ... + if len(values) > 1: + self.logger.warning( + ( _("Only one script name is allowed in a script directive, the first one is used. (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + + script_name = values[0] + + if script_name in self.scripts: + self.logger.warning( + ( _("Script name '%(name)s' is allready declared, it will be overwritten. (file '%(file)s', line %(lnr)s)") + % {'name': script_name, 'file': filename, 'lnr': linenr}) + ) + + self.scripts[script_name] = LogRotateScript( + name = script_name, + local_dir = self.local_dir, + verbose = self.verbose, + test_mode = self.test_mode, + ) + #self.scripts[script_name]['cmd'] = [] + #self.scripts[script_name]['post_files'] = 0 + #self.scripts[script_name]['last_files'] = 0 + #self.scripts[script_name]['first'] = False + #self.scripts[script_name]['prerun'] = False + #self.scripts[script_name]['donepost'] = False + #self.scripts[script_name]['donelast'] = False + + return script_name + + #------------------------------------------------------------ + def _do_include( self, line, rest, filename, linenr): + ''' + Starts a new logfile definition. + It raises a LogrotateConfigurationError on error. + + @param line: line of current config file + @type line: str + @param rest: rest of the current line after »include« + @type rest: str + @param filename: current configuration file + @type filename: str + @param linenr: current line number of configuration file + @type linenr: int + + @return: Success of include + @rtype: bool + ''' + + _ = self.t.lgettext + + # split the rest in chunks + values = split_parts(rest) + + # insufficient arguments to include ... + if len(values) < 1: + self.logger.warning( + ( _("No file or directory given in a include directive (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + return False + + # to much arguments to include ... + if len(values) > 1: + self.logger.warning( + ( _("Only one declaration of a file or directory is allowed in a include directive, the first one is used. (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + + include = values[0] + + # including object doesn't exists + if not os.path.exists(include): + self.logger.warning( + ( _("Including object '%(include)s' doesn't exists. (file '%(file)s', line %(lnr)s)") + % {'include': include, 'file': filename, 'lnr': linenr}) + ) + return False + + include = os.path.abspath(include) + + # including object is neither a regular file nor a directory + if not (os.path.isfile(include) or os.path.isdir(include)): + self.logger.warning( + ( _("Including object '%(include)s' is neither a regular file nor a directory. (file '%(file)s', line %(lnr)s)") + % {'include': include, 'file': filename, 'lnr': linenr}) + ) + return False + + if self.verbose > 1: + self.logger.debug( ( _("Trying to include object '%s' ...") % (include) )) + + # including object is a regular file + if os.path.isfile(include): + if include in self.config_files: + self.logger.warning( + ( _("Recursive including of '%(include)s'. (file '%(file)s', line %(lnr)s)") + % {'include': include, 'file': filename, 'lnr': linenr}) + ) + return False + return self._read(include) + + # This should never happen ... + if not os.path.isdir(include): + raise Exception( + ( _("What the hell is this: '%(include)s'. (file '%(file)s', line %(lnr)s)") + % {'include': include, 'file': filename, 'lnr': linenr}) + ) + + # including object is a directory - include all files + if self.verbose > 1: + self.logger.debug( ( _("Including directory '%s' ...") % (include) )) + + dir_list = os.listdir(include) + for item in sorted(dir_list, key=str.lower): + + item_path = os.path.abspath(os.path.join(include, item)) + if self.verbose > 2: + self.logger.debug( ( _( "Including item '%(item)s' ('%(path)s') ..." ) + % {'item': item, 'path': item_path} ) + ) + + # Skip directories + if os.path.isdir(item_path): + if self.verbose > 1: + self.logger.debug( ( _("Skip subdirectory '%s' in including.") % (item_path))) + continue + + # Skip non regular files + if not os.path.isfile(item_path): + self.logger.debug( ( _("Item '%s' is not a regular file.") % (item_path))) + continue + + # Check for taboo pattern + taboo_found = False + for pattern in self.taboo: + match = re.search(pattern, item) + if match: + if self.verbose > 1: + self.logger.debug( + ( _("Item '%(item)s' is matching pattern '%(pattern)s', skiping.") + % {'item': item, 'pattern': pattern}) + ) + taboo_found = True + break + if taboo_found: + continue + + # Check, whther it was former included + if item_path in self.config_files: + self.logger.warning( + ( _("Recursive including of '%(include)s' (file '%(file)s', line %(lnr)s)") + % {'include': item_path, 'file': filename, 'lnr': linenr}) + ) + return False + self._read(item_path) + + #------------------------------------------------------------ + def _start_logfile_definition( + self, line, filename, in_fd, in_logfile_list, linenr + ): + ''' + Starts a new logfile definition. + It raises a LogrotateConfigurationError on error. + + @param line: line of current config file + @type line: str + @param filename: current configuration file + @type filename: str + @param in_fd: parsing inside a logfile definition + @type in_fd: bool + @param in_logfile_list: logfile pattern list was started + @type in_logfile_list: bool + @param linenr: current line number of configuration file + @type linenr: int + + @return: name of the script (if a new script definition) or None + @rtype: str or None + ''' + + _ = self.t.lgettext + + if in_fd: + raise LogrotateConfigurationError( + ( _("Nested logfile definitions are not allowed. (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + + if not in_logfile_list: + raise LogrotateConfigurationError( + ( _("No logfile pattern defined on starting a logfile definition. (file '%(file)s', line %(lnr)s)") + % {'file': filename, 'lnr': linenr}) + ) + + #------------------------------------------------------------ + def _start_log_script_definition( self, script_type, script_name, line, filename, in_fd, linenr): + ''' + Starts a new logfile definition or logfile refrence + inside a logfile definition. + It raises a LogrotateConfigurationError outside a logfile definition. + + @param script_type: postrotate, prerotate, firstaction + or lastaction + @type script_type: str + @param script_name: name of refernced script + @type script_name: str or None + @param line: line of current config file + @type line: str + @param filename: current configuration file + @type filename: str + @param in_fd: parsing inside a logfile definition + @type in_fd: bool + @param linenr: current line number of configuration file + @type linenr: int + + @return: name of the script (if a new script definition) or None + @rtype: str or None + ''' + + _ = self.t.lgettext + + if not in_fd: + raise LogrotateConfigurationError( + ( _("Directive '%(directive)s' is not allowed outside of a logfile definition. (file '%(file)s', line %(lnr)s)") + % {'directive': script_type, 'file': filename, 'lnr': linenr}) + ) + + if script_name: + self.new_log[script_type] = script_name + return None + + new_script_name = self._new_scriptname(script_type) + + self.scripts[new_script_name] = LogRotateScript( + name = new_script_name, + local_dir = self.local_dir, + verbose = self.verbose, + test_mode = self.test_mode, + ) + #self.scripts[new_script_name] = {} + #self.scripts[new_script_name]['cmd'] = [] + #self.scripts[new_script_name]['post_files'] = 0 + #self.scripts[new_script_name]['last_files'] = 0 + #self.scripts[new_script_name]['first'] = False + #self.scripts[new_script_name]['prerun'] = False + #self.scripts[new_script_name]['donepost'] = False + #self.scripts[new_script_name]['donelast'] = False + + self.new_log[script_type] = new_script_name + + return new_script_name + + #------------------------------------------------------------ + def _new_scriptname(self, script_type = 'script'): + ''' + Retrieves a new, unique script name. + + @param script_type: prefix of the script name + @type script_type: str + + @return: a new, unique script name + @rtype: str + ''' + + i = 0 + template = script_type + "_%02d" + name = template % (i) + + while True: + + if name in self.scripts: + cmd = self.scripts[name].cmd + if cmd is not None: + if len(cmd): + i += 1 + name = template % (i) + else: + break + else: + break + else: + break + + return name + + #------------------------------------------------------------ + def _start_new_log(self, config_file, rownum): + ''' + Starting a new log definition in self.new_log and filling it + with the current default values. + + @param config_file: the configuration file with the start + of the logfile definition + @type config_file: str + @param rownum: the row number of the configuration file + with the start of the logfile definition + @type rownum: int + ''' + + _ = self.t.lgettext + + if self.verbose > 3: + self.logger.debug( _("Starting a new log directive with default values.")) + + self.new_log = {} + + self.new_log['files'] = [] + self.new_log['file_patterns'] = [] + + self.new_log['compress'] = self.default['compress'] + self.new_log['compresscmd'] = self.default['compresscmd'] + self.new_log['compressext'] = self.default['compressext'] + self.new_log['compressoptions'] = self.default['compressoptions'] + self.new_log['configfile'] = config_file + self.new_log['configrow'] = rownum + self.new_log['copy'] = self.default['copy'] + self.new_log['copytruncate'] = self.default['copytruncate'] + self.new_log['create'] = { + 'enabled': self.default['create']['enabled'], + 'mode': self.default['create']['mode'], + 'owner': self.default['create']['owner'], + 'group': self.default['create']['group'], + } + self.new_log['period'] = self.default['period'] + self.new_log['dateext'] = self.default['dateext'] + self.new_log['datepattern'] = self.default['datepattern'] + self.new_log['delaycompress'] = self.default['delaycompress'] + self.new_log['extension'] = self.default['extension'] + self.new_log['ifempty'] = self.default['ifempty'] + self.new_log['mailaddress'] = self.default['mailaddress'] + self.new_log['mailfirst'] = self.default['mailfirst'] + self.new_log['maxage'] = self.default['maxage'] + self.new_log['missingok'] = self.default['missingok'] + self.new_log['olddir'] = { + 'dirname': self.default['olddir']['dirname'], + 'dateformat': self.default['olddir']['dateformat'], + 'enabled': self.default['olddir']['enabled'], + 'mode': self.default['olddir']['mode'], + 'owner': self.default['olddir']['owner'], + 'group': self.default['olddir']['group'], + } + self.new_log['rotate'] = self.default['rotate'] + self.new_log['sharedscripts'] = self.default['sharedscripts'] + self.new_log['shred'] = self.default['shred'] + self.new_log['size'] = self.default['size'] + self.new_log['start'] = self.default['start'] + + for script_type in script_directives: + self.new_log[script_type] = None + + #------------------------------------------------------------ + def _assign_logfiles(self): + ''' + Finds all existing logfiles of self.new_log according to the + shell matching patterns in self.new_log['file_patterns']. + If a logfile was even defined, a warning is omitted and the + new definition will thrown away. + + @return: number of found logfiles according to self.new_log['file_patterns'] + @rtype: int + ''' + + _ = self.t.lgettext + + if len(self.new_log['file_patterns']) <= 0: + msg = _("No logfile pattern defined.") + self.logger.warning(msg) + return 0 + + for pattern in self.new_log['file_patterns']: + if self.verbose > 1: + msg = _("Find all logfiles for shell matching pattern '%s' ...") \ + % (pattern) + self.logger.debug(msg) + logfiles = glob.glob(pattern) + if len(logfiles) <= 0: + msg = _("No logfile found for pattern '%s'.") % (pattern) + if self.new_log['missingok']: + self.logger.debug(msg) + else: + self.logger.warning(msg) + continue + for logfile in logfiles: + if self.verbose > 1: + msg = _("Found logfile '%(file)s for pattern '%(pattern)s'.") \ + % {'file': logfile, 'pattern': pattern } + self.logger.debug(msg) + if logfile in self.defined_logfiles: + f = self.defined_logfiles[logfile] + msg = ( _("Logfile '%(logfile)s' is even defined (file '%(cfgfile)s', " + + "row %(rownum)d) and so not taken a second time.") + % {'logfile': logfile, + 'cfgfile': f['file'], + 'rownum': f['rownum']} + ) + self.logger.warning(msg) + continue + if self.verbose > 1: + msg = _("Logfile '%s' will taken.") \ + % (logfile) + self.defined_logfiles[logfile] = { + 'file': self.new_log['configfile'], + 'rownum': self.new_log['configrow'], + } + self.new_log['files'].append(logfile) + + return len(self.new_log['files']) + +#======================================================================== + +if __name__ == "__main__": + pass + + +#======================================================================== + +# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotate/LogRotateGetopts.py b/LogRotate/LogRotateGetopts.py new file mode 100755 index 0000000..e74bde7 --- /dev/null +++ b/LogRotate/LogRotateGetopts.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# $Id$ +# $URL$ + +''' +@author: Frank Brehm +@contact: frank@brehm-online.com +@license: GPL3 +@copyright: (c) 2010-2011 by Frank Brehm, Berlin +@version: 0.0.1 +@summary: Option parser for Python logrotating +''' + +import re +import sys +import gettext + +from optparse import OptionError +from optparse import OptionParser +from optparse import OptionGroup +from optparse import OptionConflictError + + +revision = '$Revision$' +revision = re.sub( r'\$', '', revision ) +revision = re.sub( r'Revision: ', r'r', revision ) +revision = re.sub( r'\s*$', '', revision ) + +__author__ = 'Frank Brehm' +__copyright__ = '(C) 2011 by Frank Brehm, Berlin' +__contact__ = 'frank@brehm-online.com' +__version__ = '0.0.1 ' + revision +__license__ = 'GPL3' + + +#======================================================================== + +class LogrotateOptParserError(Exception): + ''' + Class for exceptions in this module, escpacially + due to false commandline options. + ''' + +#======================================================================== + +class LogrotateOptParser(object): + ''' + Class for parsing commandline options of Python logrotating. + + @author: Frank Brehm + @contact: frank@brehm-online.com + ''' + + #------------------------------------------------------- + def __init__( self, prog = '%prog', + version = None, + local_dir = None, + ): + ''' + Constructor. + + @param prog: The name of the calling process (e.g. sys.argv[0]) + @type prog: str + @param version: The version string to use + @type version: str + @param local_dir: The directory, where the i18n-files (*.mo) + are located. If None, then system default + (/usr/share/locale) is used. + @type local_dir: str or None + + @return: None + ''' + + self.prog = prog + ''' + @ivar: The name of the calling process + @type: str + ''' + + self.version = version + ''' + @ivar: The version string to use + @type: str + ''' + + self.local_dir = local_dir + ''' + @ivar: The directory, where the i18n-files (*.mo) are located. + @type: str or None + ''' + + self.t = gettext.translation( + 'LogRotateGetopts', + local_dir, + fallback = True + ) + ''' + @ivar: a gettext translation object + @type: gettext.translation + ''' + + _ = self.t.lgettext + + self.description = _('Rotates, compresses and mails system logs.') + ''' + @ivar: description of the program + @type: str + ''' + + self.usage = ( _("%s [options] ") + "\n" ) %(prog) + ''' + @ivar: the usage string in getopt help output + @type: str + ''' + self.usage += ( ' %s [-h|-?|--help]\n' %(prog) ) + self.usage += ( ' %s --usage\n' %(prog) ) + self.usage += ( ' %s --version' %(prog) ) + + self.options = None + ''' + @ivar: a dict with all given commandline options + after calling getOpts() + @type: dict or None + ''' + + self.args = None + ''' + @ivar: a list with all commandline parameters, what are not options + @type: list or None + ''' + + self.parsed = False + ''' + @ivar: flag, whether the parsing was done + @type: bool + ''' + + if version: + self.version = version + + self.parser = OptionParser( + prog = self.prog, + version = self.version, + description = self.description, + usage = self.usage, + conflict_handler = "resolve", + ) + ''' + @ivar: the working OptionParser Object + @type: optparse.OptionParser + ''' + + self._add_options() + + #------------------------------------------------------- + def _add_options(self): + ''' + Private function to add all necessary options + to the OptionParser object + ''' + + _ = self.t.ugettext + __ = self.t.ungettext + + if self.parser.has_option('--help'): + self.parser.remove_option('--help') + + if self.parser.has_option('--version'): + self.parser.remove_option('--version') + + self.parser.add_option( + '--simulate', + '--test', + '-T', + default = False, + action = 'store_true', + dest = 'test', + help = _('set this do simulate commands'), + ) + + self.parser.add_option( + '--verbose', + '-v', + default = False, + action = 'count', + dest = 'verbose', + help = _('set the verbosity level'), + ) + + self.parser.add_option( + '--debug', + '-d', + default = False, + action = 'store_true', + dest = 'debug', + help = _("Don't do anything, just test (implies -v and -T)"), + ) + + self.parser.add_option( + '--force', + '-f', + default = False, + action = 'store_true', + dest = 'force', + help = _("Force file rotation"), + ) + + self.parser.add_option( + '--config-check', + '-c', + default = False, + action = 'store_true', + dest = 'configcheck', + help = _("Checks only the given configuration file and does " + + "nothing. Conflicts with -f."), + ) + + self.parser.add_option( + '--state', + '-s', + dest = "statefile", + metavar = 'FILE', + help = _('Path of state file (different to configuration)'), + ) + + self.parser.add_option( + '--pid-file', + '-P', + dest = "pidfile", + metavar = 'FILE', + help = _('Path of PID file (different to configuration)'), + ) + + self.parser.add_option( + '--mail', + '-m', + dest = "mailcmd", + metavar = 'CMD', + help = _('Command to send mail (instead of using ' + + 'the Phyton email package)'), + ) + + ###### + # Option group for common options + + group = OptionGroup(self.parser, _("Common options")) + + group.add_option( + '-h', + '-?', + '--help', + default = False, + action = 'help', + dest = 'help', + help = _('Shows a help message and exit.'), + ) + + group.add_option( + '--usage', + default = False, + action = 'store_true', + dest = 'usage', + help = _('Display brief usage message and exit.'), + ) + + group.add_option( + '-V', + '--version', + default = False, + action = 'version', + dest = 'version', + help = _('Shows the version number of the program and exit.'), + ) + + self.parser.add_option_group(group) + + #---------------------------------------------------------------------- + def getOpts(self): + ''' + Wrapper function to OptionParser.parse_args(). + Sets self.options and self.args with the appropriate values. + @return: None + ''' + + _ = self.t.ugettext + + if not self.parsed: + self.options, self.args = self.parser.parse_args() + self.parsed = True + + if self.options.usage: + self.parser.print_usage() + sys.exit(0) + + if self.options.force and self.options.configcheck: + raise LogrotateOptParserError( _('Invalid usage of --force and ' + + '--config-check.') ) + + if self.args is None or len(self.args) < 1: + raise LogrotateOptParserError( _('No configuration file given.') ) + + if len(self.args) != 1: + raise LogrotateOptParserError( + _('Only one configuration file is allowed.') + ) + +#======================================================================== + +if __name__ == "__main__": + pass + + +#======================================================================== + +# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotate/LogRotateHandler.py b/LogRotate/LogRotateHandler.py new file mode 100755 index 0000000..dda1852 --- /dev/null +++ b/LogRotate/LogRotateHandler.py @@ -0,0 +1,2390 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# $Id$ +# $URL$ + +''' +@author: Frank Brehm +@contact: frank@brehm-online.com +@license: GPL3 +@copyright: (c) 2010-2011 by Frank Brehm, Berlin +@version: 0.4.0 +@summary: Application handler module for Python logrotating +''' + +# Für Terminal-Dinge: http://code.activestate.com/recipes/475116/ + +import re +import sys +import gettext +import logging +import pprint +import os +import os.path +import errno +import socket +import subprocess +import shutil +import glob +from datetime import datetime, timedelta +import time +import gzip +import bz2 +import zipfile + +from LogRotateConfig import LogrotateConfigurationError +from LogRotateConfig import LogrotateConfigurationReader + +from LogRotateStatusFile import LogrotateStatusFileError +from LogRotateStatusFile import LogrotateStatusFile +from LogRotateStatusFile import utc + +from LogRotateMailer import LogRotateMailerError +from LogRotateMailer import LogRotateMailer + +revision = '$Revision$' +revision = re.sub( r'\$', '', revision ) +revision = re.sub( r'Revision: ', r'r', revision ) +revision = re.sub( r'\s*$', '', revision ) + +__author__ = 'Frank Brehm' +__copyright__ = '(C) 2011 by Frank Brehm, Berlin' +__contact__ = 'frank@brehm-online.com' +__version__ = '0.4.0 ' + revision +__license__ = 'GPL3' + + +#======================================================================== + +class LogrotateHandlerError(Exception): + ''' + Base class for exceptions in this module. + ''' + +#======================================================================== + +class StdoutFilter(logging.Filter): + ''' + Class, that filters all logrecords + ''' + + def filter(self, record): + ''' + Filtering log records and let through messages + except them with the level names 'WARNING', 'ERROR' or 'CRITICAL'. + + @param record: the record to filter + @type record: logging.LogRecord + + @return: pass the record or not + ''' + if record.levelname == 'WARNING': + return False + if record.levelname == 'ERROR': + return False + if record.levelname == 'CRITICAL': + return False + return True + +#======================================================================== + +class LogrotateHandler(object): + ''' + Class for application handler for Python logrotating + + @author: Frank Brehm + @contact: frank@brehm-online.com + ''' + + #------------------------------------------------------- + def __init__( self, config_file, + test = False, + verbose = 0, + force = False, + config_check = False, + state_file = None, + pid_file = None, + mail_cmd = None, + local_dir = None, + version = None, + ): + ''' + Constructor. + + @param config_file: the configuration file to use + @type config_file: str + @param test: testmode, no real actions are made + @type test: bool + @param verbose: verbosity (debug) level + @type verbose: int + @param force: Force file rotation + @type force: bool + @param config_check: Checks only the configuration and does nothing + @type config_check: bool + @param state_file: Path of state file (different to configuration) + @type state_file: str or None + @param pid_file: Path of PID file (different to configuration) + @type pid_file: str or None + @param mail_cmd: command to send mail (instead of using + the Phyton email package) + @type mail_cmd: str or None + @param local_dir: The directory, where the i18n-files (*.mo) + are located. If None, then system default + (/usr/share/locale) is used. + @type local_dir: str or None + @param version: version number to show + @type version: str + + @return: None + ''' + + self.local_dir = local_dir + ''' + @ivar: The directory, where the i18n-files (*.mo) are located. + @type: str or None + ''' + + self.t = gettext.translation( + 'LogRotateHandler', + local_dir, + fallback = True + ) + ''' + @ivar: a gettext translation object + @type: gettext.translation + ''' + + _ = self.t.lgettext + + self.verbose = verbose + ''' + @ivar: verbosity level (0 - 9) + @type: int + ''' + + self.version = __version__ + ''' + @ivar: version number to show, e.g. as the X-Mailer version + @type: str + ''' + if version is not None: + self.version = version + + self.test = test + ''' + @ivar: testmode, no real actions are made + @type: bool + ''' + + self.force = force + ''' + @ivar: Force file rotation + @type: bool + ''' + + self.state_file = None + ''' + @ivar: the state file object after his initialisation + @type: LogRotateStateFile or None + ''' + + self.state_file_name = state_file + ''' + @ivar: Path of state file (from commandline or from configuration) + @type: str + ''' + + self.pid_file = pid_file + ''' + @ivar: Path of PID file (from commandline or from configuration) + @type: str + ''' + + self.pidfile_created = False + ''' + @ivar: Is a PID file created by this instance and should removed + on destroying this object. + @type: bool + ''' + + self.mail_cmd = mail_cmd + ''' + @ivar: command to send mail (instead of using the Phyton email package) + @type: str or None + ''' + + self.config_file = config_file + ''' + @ivar: the initial configuration file to use + @type: str + ''' + + self.config = [] + ''' + @ivar: the configuration, how it was read from cofiguration file(s) + @type: dict + ''' + + self.scripts = {} + ''' + @ivar: list of LogRotateScript objects with all named scripts found in configuration + @type: list + ''' + + self.template = {} + ''' + @ivar: things to do in olddir stuff + @type: dict + ''' + self._prepare_templates() + + self.logfiles = [] + ''' + @ivar: list of all rotated logfiles. Each entry is a dict with + three keys: + - 'original': str with the name of the unrotated file + - 'rotated': str with the name of the rotated file + - 'oldfiles: list with all old rotated files of this file + - 'desc_index': index of list self.config for appropriate + logfile definition + @type: list + ''' + + self.files_delete = {} + ''' + @ivar: dictionary with all files, they have to delete + @type: dict + ''' + + self.files_compress = {} + ''' + @ivar: dictionary with all files, they have to compress + keys are the filenames, values are the index number + of the list self.config (for compress options) + @type: dict + ''' + + self.files2send = {} + ''' + @ivar: dictionary with all all rotated logfiles to send via + mail to one or more recipients. + Keys are the file names of the (even existing) rotated + and maybe compressed logfiles. + Values are a tuple of (mailaddress, original_logfile), where + mailaddress is a comma separated list of mail addresses of + the recipients of the mails, and original_logfile is the name + of unrotated logfile. + This dict will filled by _do_rotate_file(), and will performed + by send_logfiles(). + @type: dict + ''' + + ################################################# + # Create a logger object + self.logger = logging.getLogger('pylogrotate') + ''' + @ivar: logger object + @type: logging.getLogger + ''' + + self.logger.setLevel(logging.DEBUG) + + # create formatter + format_str = '[%(asctime)s]: %(levelname)-8s - %(message)s' + if test: + format_str = '%(levelname)-8s - %(message)s' + if verbose: + if verbose > 1: + format_str = '[%(asctime)s]: %(name)s %(funcName)s() %(levelname)-8s - %(message)s' + if test: + format_str = '%(name)s %(funcName)s() %(levelname)-8s - %(message)s' + else: + format_str = '[%(asctime)s]: %(name)s %(levelname)-8s - %(message)s' + if test: + format_str = '%(name)s %(levelname)-8s - %(message)s' + formatter = logging.Formatter(format_str) + + # create console handler for error messages + console_stderr = logging.StreamHandler(sys.stderr) + console_stderr.setLevel(logging.WARNING) + console_stderr.setFormatter(formatter) + self.logger.addHandler(console_stderr) + + # create console handler for other messages + console_stdout = logging.StreamHandler(sys.stdout) + if verbose: + console_stdout.setLevel(logging.DEBUG) + else: + console_stdout.setLevel(logging.INFO) + fltr = StdoutFilter() + console_stdout.addFilter(fltr) + console_stdout.setFormatter(formatter) + self.logger.addHandler(console_stdout) + + # define a mailer object + self.mailer = LogRotateMailer( + local_dir = self.local_dir, + verbose = self.verbose, + test_mode = self.test, + mailer_version = self.version, + ) + if mail_cmd: + self.mailer.sendmail = mail_cmd + + # end of init properties + self.logger.debug( _("Logrotating initialised") ) + + if not self.read_configuration(): + self.logger.error( _('Could not read configuration') ) + sys.exit(1) + + if config_check: + return + + if not self._check_pidfile(): + sys.exit(3) + + if not self._write_pidfile(): + sys.exit(3) + + self.logger.debug( _("Logrotating ready for work") ) + + # Create status file object + self.state_file = LogrotateStatusFile( + file_name = self.state_file_name, + local_dir = self.local_dir, + verbose = self.verbose, + test_mode = self.test, + ) + + #------------------------------------------------------------ + def __str__(self): + ''' + Typecasting function for translating object structure + into a string + + @return: structure as string + @rtype: str + ''' + + pp = pprint.PrettyPrinter(indent=4) + structure = self.as_dict() + return pp.pformat(structure) + + #------------------------------------------------------- + def as_dict(self): + ''' + Transforms the elements of the object into a dict + + @return: structure as dict + @rtype: dict + ''' + + res = { + 'config': self.config, + 'config_file': self.config_file, + 'files_delete': self.files_delete, + 'files_compress': self.files_compress, + 'files2send': self.files2send, + 'force': self.force, + 'local_dir': self.local_dir, + 'logfiles': self.logfiles, + 'logger': self.logger, + 'mail_cmd': self.mail_cmd, + 'mailer': self.mailer.as_dict(), + 'scripts': {}, + 'state_file': None, + 'state_file_name': self.state_file_name, + 'pid_file': self.pid_file, + 'pidfile_created': self.pidfile_created, + 't': self.t, + 'test': self.test, + 'template': self.template, + 'verbose': self.verbose, + 'version': self.version, + } + if self.state_file: + res['state_file'] = self.state_file.as_dict() + + for script_name in self.scripts.keys(): + res['scripts'][script_name] = self.scripts[script_name].as_dict() + + return res + + #------------------------------------------------------------ + def __del__(self): + ''' + Destructor. + No parameters, no return value. + ''' + + _ = self.t.lgettext + + if self.pidfile_created: + if os.path.exists(self.pid_file): + self.logger.debug( _("Removing PID file '%s' ...") % (self.pid_file) ) + try: + os.remove(self.pid_file) + except OSError, e: + self.logger.error( _("Error removing PID file '%(file)s': %(msg)") + % { 'file': self.pid_file, 'msg': str(e) } + ) + + #------------------------------------------------------------ + def _prepare_templates(self): + ''' + Preparing self.template with values for placeholders + in olddir stuff. + ''' + + self.template = {} + + hostname = socket.getfqdn() + self.template['nodename'] = hostname + self.template['domain'] = '' + + match = re.search(r'^([^\.]+)\.(.*)', hostname) + if match: + self.template['nodename'] = match.group(1) + self.template['domain'] = match.group(2) + + uname = os.uname() + self.template['sysname'] = uname[0] + self.template['release'] = uname[2] + self.template['version'] = uname[3] + self.template['machine'] = uname[4] + + #------------------------------------------------------------ + def read_configuration(self): + ''' + Reads the configuration from self.config_file + + @return: Success of reading + @rtype: bool + ''' + + _ = self.t.lgettext + + config_reader = LogrotateConfigurationReader( + config_file = self.config_file, + verbose = self.verbose, + local_dir = self.local_dir, + test_mode = self.test, + ) + + if self.verbose > 2: + msg = _("Configuration reader object structure") + ':\n' + str(config_reader) + self.logger.debug(msg) + + try: + self.config = config_reader.get_config() + self.scripts = config_reader.get_scripts() + except LogrotateConfigurationError, e: + self.logger.error( str(e) ) + sys.exit(10) + + if self.verbose > 2: + pp = pprint.PrettyPrinter(indent=4) + msg = _("Found global options:") + "\n" + pp.pformat(config_reader.global_option) + self.logger.debug(msg) + + # Get and set mailer options + if 'mailfrom' in config_reader.global_option and \ + config_reader.global_option['mailfrom']: + self.mailer.from_address = config_reader.global_option['mailfrom'] + if config_reader.global_option['smtphost'] and \ + config_reader.global_option['smtphost'] != 'localhost': + self.mailer.smtp_host = config_reader.global_option['smtphost'] + if 'smtpport' in config_reader.global_option: + self.mailer.smtp_port = config_reader.global_option['smtpport'] + if 'smtptls' in config_reader.global_option: + self.mailer.smtp_tls = config_reader.global_option['smtptls'] + if 'smtpuser' in config_reader.global_option: + self.mailer.smtp_user = config_reader.global_option['smtpuser'] + if 'smtppasswd' in config_reader.global_option: + self.mailer.smtp_passwd = config_reader.global_option['smtppasswd'] + + if self.state_file_name is None: + if 'statusfile' in config_reader.global_option and \ + config_reader.global_option['statusfile'] is not None: + self.state_file_name = config_reader.global_option['statusfile'] + else: + self.state_file_name = os.sep + os.path.join('var', 'lib', 'py-logrotate.status') + self.logger.debug( _("Name of state file: '%s'") % (self.state_file_name) ) + + if self.pid_file is None: + if 'pidfile' in config_reader.global_option and \ + config_reader.global_option['pidfile'] is not None: + self.pid_file = config_reader.global_option['pidfile'] + else: + self.pid_file = os.sep + os.path.join('var', 'run', 'py-logrotate.pid') + self.logger.debug( _("PID file: '%s'") % (self.pid_file) ) + + return True + + #------------------------------------------------------------ + def _check_pidfile(self): + ''' + Checks the existence and consistence of self.pid_file. + + Exit, if there is a running process with a PID from this file. + Doesn't exit in test mode. + + Writes on success (no other process) this PID file. + + @return: Success + @rtype: bool + ''' + + _ = self.t.lgettext + + if not os.path.exists(self.pid_file): + if self.verbose > 1: + self.logger.debug( _("PID file '%s' doesn't exists.") % (self.pid_file) ) + return True + + if self.test: + self.logger.info( _("Testmode, skip test of PID file '%s'.") % (self.pid_file) ) + return True + + self.logger.debug( _("Reading PID file '%s' ...") % (self.pid_file) ) + f = None + try: + f = open(self.pid_file, 'r') + except IOError, e: + raise LogrotateHandlerError( + _("Couldn't open PID file '%(file)s' for reading: %(msg)s") + % { 'file': self.pid_file, 'msg': str(e) } + ) + + line = f.readline() + f.close() + + pid = None + line = line.strip() + match = re.search(r'^\s*(\d+)\s*$', line) + if match: + pid = int(match.group(1)) + else: + self.logger.warn( _("No useful information found in PID file '%(file)s': '%(line)s'") + % { 'file': self.pid_file, 'line': line } + ) + return False + + if self.verbose > 1: + self.logger.debug( _("Trying check for process with PID %d ...") % (pid) ) + try: + os.kill(pid, 0) + except OSError, err: + if err.errno == errno.ESRCH: + self.logger.info( _("Process with PID %d anonymous died.") % (pid) ) + return True + elif err.errno == errno.EPERM: + self.logger.warn( _("No permission to signal the process %d ...") % (pid) ) + return True + else: + self.logger.warn( _("Unknown error: '%s'") % (str(err)) ) + return False + else: + self.logger.error( _("Process with PID %d is allready running.") % (pid) ) + return False + + return False + + #------------------------------------------------------------ + def _write_pidfile(self): + ''' + Writes the PID of the current process in self.pid_file. + + Exit with an error, if it's not possible to write. + Doesn't exit in test mode. + + Writes on success (no other process) this PID file. + + @return: Success + @rtype: bool + ''' + + _ = self.t.lgettext + + if self.test: + self.logger.info( _("Testmode, skip writing of PID file '%s'.") % (self.pid_file) ) + return True + + self.logger.debug( _("Writing PID file '%s' ...") % (self.pid_file) ) + + f = None + try: + f = open(self.pid_file, 'w') + f.write(str(os.getppid()) + "\n") + f.close() + except IOError, e: + raise LogrotateHandlerError( + _("Couldn't open PID file '%(file)s' for writing: %(msg)s") + % { 'file': self.pid_file, 'msg': str(e) } + ) + + self.pidfile_created = True + + return True + + #------------------------------------------------------------ + def rotate(self): + ''' + Starting the underlying rotating. + + @return: None + ''' + + _ = self.t.lgettext + + if len(self.config) < 1: + msg = _("No logfile definitions found.") + self.logger.info(msg) + return + + msg = _("Starting underlying rotation ...") + self.logger.info(msg) + + cur_desc_index = 0 + for d in self.config: + self._rotate_definition(cur_desc_index) + cur_desc_index += 1 + + if self.verbose > 1: + line = 60 * '-' + print line + "\n\n" + + # Check for left over scripts to execute + for scriptname in self.scripts.keys(): + if self.verbose >= 4: + msg = ( _("State of script '%s':") % (scriptname) ) \ + + "\n" + str(self.scripts[scriptname]) + self.logger.debug(msg) + del self.scripts[scriptname] + + return + + #------------------------------------------------------------ + def _rotate_definition(self, cur_desc_index): + ''' + Rotation of a logfile definition from a configuration file. + + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + + @return: None + ''' + + definition = self.config[cur_desc_index] + + _ = self.t.lgettext + + if self.verbose > 1: + line = 60 * '-' + print line + "\n\n" + + if self.verbose >= 4: + pp = pprint.PrettyPrinter(indent=4) + msg = _("Rotating of logfile definition:") + \ + "\n" + pp.pformat(definition) + self.logger.debug(msg) + + # re-reading of status file + self.state_file.read() + + for logfile in definition['files']: + if self.verbose > 1: + line = 30 * '-' + print (line + "\n") + msg = ( _("Performing logfile '%s' ...") % (logfile)) + self.logger.debug(msg) + should_rotate = self._should_rotate(logfile, cur_desc_index) + if self.verbose > 1: + if should_rotate: + msg = _("logfile '%s' WILL rotated.") + else: + msg = _("logfile '%s' will NOT rotated.") + self.logger.debug(msg % (logfile)) + if not should_rotate: + continue + self._rotate_file(logfile, cur_desc_index) + + if self.verbose > 1: + print "\n" + + return + + #------------------------------------------------------------ + def _rotate_file(self, logfile, cur_desc_index): + ''' + Rotates a logfile with all with all necessary actions before + and after rotation. + + Throughs an LogrotateHandlerError on error. + + @param logfile: the logfile to rotate + @type logfile: str + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + + @return: None + ''' + + definition = self.config[cur_desc_index] + + _ = self.t.lgettext + + sharedscripts = definition['sharedscripts'] + firstscript = definition['firstaction'] + prescript = definition['prerotate'] + postscript = definition['postrotate'] + lastscript = definition['lastaction'] + + # Executing of the firstaction script, if it wasn't executed + if firstscript: + if self.verbose > 2: + msg = _("Looking, whether the firstaction script should be executed.") + self.logger.debug(msg) + if not self.scripts[firstscript].done_firstrun: + msg = _("Executing firstaction script '%s' ...") % (firstscript) + self.logger.info(msg) + if not self.scripts[firstscript].execute(): + return + self.scripts[firstscript].done_firstrun = True + + # Executing prerotate scripts, if not sharedscripts or even not executed + if prescript: + if self.verbose > 2: + msg = _("Looking, whether the prerun script should be executed.") + self.logger.debug(msg) + do_it = False + if sharedscripts: + if not self.scripts[prescript].done_prerun: + do_it = True + else: + do_it = True + if do_it: + msg = _("Executing prerun script '%s' ...") % (prescript) + self.logger.info(msg) + if not self.scripts[prescript].execute(): + return + self.scripts[prescript].done_prerun = True + + olddir = self._create_olddir(logfile, cur_desc_index) + if olddir is None: + return + + if not self._do_rotate_file(logfile, cur_desc_index, olddir): + return + + # Looking for postrotate script in a similar way like for the prerotate + if postscript: + if self.verbose > 2: + msg = _("Looking, whether the postrun script should be executed.") + self.logger.debug(msg) + do_it = False + self.scripts[postscript].post_files -= 1 + self.scripts[postscript].do_post = True + if sharedscripts: + if self.scripts[postscript].post_files <= 0: + do_it = True + self.scripts[postscript].do_post = False + else: + do_it = True + if do_it: + msg = _("Executing postrun script '%s' ...") % (postscript) + self.logger.info(msg) + if not self.scripts[postscript].execute(): + return + self.scripts[postscript].done_postrun = True + + # Looking for lastaction script + if lastscript: + if self.verbose > 2: + msg = _("Looking, whether the lastaction script should be executed.") + self.logger.debug(msg) + do_it = False + self.scripts[lastscript].last_files -= 1 + self.scripts[lastscript].do_last = True + if self.scripts[lastscript].done_lastrun: + self.scripts[lastscript].do_last = False + else: + if self.scripts[lastscript].last_files <= 0: + do_it = True + self.scripts[lastscript].do_last = False + if do_it: + msg = _("Executing lastaction script '%s' ...") % (lastscript) + self.logger.info(msg) + if not self.scripts[lastscript].execute(): + return + self.scripts[lastscript].done_lastrun = True + + #------------------------------------------------------------ + def _do_rotate_file(self, logfile, cur_desc_index, olddir = None): + ''' + The underlaying unconditionally rotation of a logfile. + + After the successful rotation + + @param logfile: the logfile to rotate + @type logfile: str + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + @param olddir: the directory of the rotated logfile + if "." or None, store the rotated logfile + in their original directory + @type olddir: str or None + + @return: successful or not + @rtype: bool + ''' + + definition = self.config[cur_desc_index] + + if (olddir is not None) and (olddir == "."): + olddir = None + + _ = self.t.lgettext + + uid = os.geteuid() + gid = os.getegid() + + msg = _("Do rotate logfile '%s' ...") % (logfile) + self.logger.debug(msg) + + target = self._get_rotation_target(logfile, cur_desc_index, olddir) + rotations = self._get_rotations(logfile, target, cur_desc_index) + + extension = rotations['extension'] + compress_extension = rotations['compress_extension'] + + # First move all cyclic stuff + for pair in rotations['move']: + file_from = pair['from'] + file_to = pair['to'] + if pair['compressed']: + file_from += compress_extension + file_to += compress_extension + msg = _("Moving file '%(from)s' => '%(to)s'.") \ + % {'from': file_from, 'to': file_to } + self.logger.info(msg) + if not self.test: + try: + shutil.move(file_from, file_to) + except OSError: + msg = _("Error on moving '%(from)s' => '%(to)s': %(err)s") \ + % {'from': file_from, 'to': file_to, 'err': e.strerror} + self.logger.error(msg) + return False + + # Now the underlaying rotation + file_from = rotations['rotate']['from'] + file_to = rotations['rotate']['to'] + + # First check for an existing mail address + if definition['mailaddress'] and definition['mailfirst']: + self.mailer.send_file(file_from, definition['mailaddress']) + + # separate between copy(truncate) and move (and create) + if definition['copytruncate'] or definition['copy']: + # Copying logfile to target + msg = _("Copying file '%(from)s' => '%(to)s'.") \ + % {'from': file_from, 'to': file_to } + self.logger.info(msg) + if not self.test: + try: + shutil.copy2(file_from, file_to) + except OSError: + msg = _("Error on copying '%(from)s' => '%(to)s': %(err)s") \ + % {'from': file_from, 'to': file_to, 'err': e.strerror} + self.logger.error(msg) + return False + if definition['copytruncate']: + msg = _("Truncating file '%s'.") % (file_from) + self.logger.info(msg) + if not self.test: + try: + fd = open(file_from, 'w') + fd.close() + except IOError, e: + msg = _("Error on truncing file '%(from)s': %(err)s") \ + % {'from': file_from, 'err': str(e)} + self.logger.error(msg) + return False + + else: + + # Moving logfile to target + msg = _("Moving file '%(from)s' => '%(to)s'.") \ + % {'from': file_from, 'to': file_to } + self.logger.info(msg) + + # get old permissions of logfile + statinfo = os.stat(file_from) + + if not self.test: + try: + shutil.move(file_from, file_to) + except OSError: + msg = _("Error on moving '%(from)s' => '%(to)s': %(err)s") \ + % {'from': file_from, 'to': file_to, 'err': e.strerror} + self.logger.error(msg) + return False + + if definition['create']['enabled']: + + # Recreate logfile + msg = _("Recreating file '%s'.") % (file_from) + self.logger.info(msg) + if not self.test: + try: + fd = open(file_from, 'w') + fd.close() + except IOError, e: + msg = _("Error on creating file '%(from)s': %(err)s") \ + % {'from': file_from, 'err': str(e)} + self.logger.error(msg) + return False + + # Setting permissions and ownership + new_mode = statinfo.st_mode + new_uid = statinfo.st_uid + new_gid = statinfo.st_gid + + if not definition['create']['mode'] is None: + new_mode = definition['create']['mode'] + if not definition['create']['owner'] is None: + new_uid = definition['create']['owner'] + if not definition['create']['group'] is None: + new_gid = definition['create']['group'] + + statinfo = os.stat(file_from) + old_mode = statinfo.st_mode + old_uid = statinfo.st_uid + old_gid = statinfo.st_gid + + # Check and set permissions of new logfile + if new_mode != old_mode: + msg = _("Setting permissions of '%(file)s' to %(mode)4o.") \ + % {'file': file_from, 'mode': new_mode} + self.logger.info(msg) + if not self.test: + try: + os.chmod(file_from, new_mode) + except OSError, e: + msg = _("Error on chmod of '%(file)s': %(err)s") \ + % {'file': file_from, 'err': e.strerror} + self.logger.warning(msg) + + # Check and set ownership of new logfile + if (new_uid != old_uid) or (new_gid != old_gid): + myuid = os.geteuid() + if myuid != 0: + msg = _("Only root may execute chown().") + if self.test: + self.logger.info(msg) + else: + self.logger.warning(msg) + else: + msg = _("Setting ownership of '%(file)s' to uid %(uid)d and gid %(gid)d.") \ + % {'file': file_from, 'uid': new_uid, 'gid': new_gid} + self.logger.info(msg) + if not self.test: + try: + os.chown(file_from, new_uid, new_gid) + except OSError, e: + msg = _("Error on chown of '%(file)s': %(err)s") \ + % {'file': file_from, 'err': e.strerror} + self.logger.warning(msg) + + oldfiles = self._collect_old_logfiles(logfile, extension, compress_extension, cur_desc_index) + + # get files to delete and save them back in self.files_delete + files_delete = self._collect_files_delete(oldfiles, cur_desc_index) + if len(files_delete): + for oldfile in files_delete: + self.files_delete[oldfile] = True + if definition['mailaddress'] and not definition['mailfirst']: + self.files2send[oldfile] = (definition['mailaddress'], logfile) + + # get files to compress save them back in self.files_compress + files_compress = self._collect_files_compress(oldfiles, compress_extension, cur_desc_index) + if len(files_compress): + for oldfile in files_compress: + self.files_compress[oldfile] = cur_desc_index + + # write back date of rotation into state file + self.state_file.set_rotation_date(logfile) + self.state_file.write() + + return True + + #------------------------------------------------------------ + def _collect_files_compress(self, oldfiles, compress_extension, cur_desc_index): + ''' + Collects a list with all old logfiles, they have to compress. + + @param oldfiles: a dict whith all found old logfiles as keys and + their modification time as values + @type oldfiles: dict + @param compress_extension: file extension for rotated and + compressed logfiles + @type compress_extension: str + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + + @return: all old (and compressed) logfiles to delete + @rtype: list + ''' + + definition = self.config[cur_desc_index] + _ = self.t.lgettext + + if self.verbose > 2: + msg = _("Retrieving logfiles to compress ...") + self.logger.debug(msg) + + result = [] + + if not definition['compress']: + if self.verbose > 3: + msg = _("No compression defined.") + self.logger.debug(msg) + return result + + if not oldfiles.keys(): + if self.verbose > 3: + msg = _("No old logfiles available.") + self.logger.debug(msg) + return result + + no_compress = definition['delaycompress'] + if no_compress is None: + no_compress = 0 + + ce = re.escape(compress_extension) + for oldfile in sorted(oldfiles.keys(), key=lambda x: oldfiles[x], reverse=True): + + match = re.search(ce + r'$', oldfile) + if match: + if self.verbose > 2: + msg = _("File '%s' seems to be compressed, skip it.") % (oldfile) + self.logger.debug(msg) + continue + + if oldfile in self.files_delete: + if self.verbose > 2: + msg = _("File '%s' will be deleted, compression unnecessary.") % (oldfile) + self.logger.debug(msg) + continue + + if no_compress: + if self.verbose > 2: + msg = _("Compression of file '%s' will be delayed.") % (oldfile) + self.logger.debug(msg) + no_compress -= 1 + continue + + result.append(oldfile) + + if self.verbose > 3: + if len(result): + pp = pprint.PrettyPrinter(indent=4) + msg = _("Found logfiles to compress:") + "\n" + pp.pformat(result) + self.logger.debug(msg) + else: + msg = _("No old logfiles to compress found.") + self.logger.debug(msg) + return result + + #------------------------------------------------------------ + def _collect_files_delete(self, oldfiles, cur_desc_index): + ''' + Collects a list with all old (and compressed) logfiles, they have to delete. + + @param oldfiles: a dict whith all found old logfiles as keys and + their modification time as values + @type oldfiles: dict + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + + @return: all old (and compressed) logfiles to delete + @rtype: list + ''' + + definition = self.config[cur_desc_index] + _ = self.t.lgettext + + if self.verbose > 2: + msg = _("Retrieving logfiles to delete ...") + self.logger.debug(msg) + + result = [] + + if not oldfiles.keys(): + if self.verbose > 3: + msg = _("No old logfiles available.") + self.logger.debug(msg) + return result + + # Maxage in seconds or None + maxage = definition['maxage'] + if maxage is None: + if self.verbose >= 4: + msg = _("No maxage given.") + self.logger.debug(msg) + else: + maxage *= (24 * 60 * 60) + if self.verbose >= 4: + msg = _("Maxage: %d seconds") % (maxage) + self.logger.debug(msg) + + # Number of rotations or Zero + rotate = definition['rotate'] + if rotate is None: + rotate = 0 + if self.verbose >= 4: + msg = _("Max. count rotations: %d") % (rotate) + self.logger.debug(msg) + + count = len(oldfiles.keys()) + for oldfile in sorted(oldfiles.keys(), key=lambda x: oldfiles[x]): + count -= 1 + age = int(time.time() - oldfiles[oldfile]) + if self.verbose > 3: + msg = _("Checking file '%s' for deleting ...") % (oldfile) + self.logger.debug(msg) + if self.verbose >= 4: + msg = _("Current count: %(count)d, current age: %(age)d seconds") \ + % {'count': count, 'age': age} + self.logger.debug(msg) + + # Delete all files, their count is more than the rotate option + if rotate: + if count >= rotate: + if self.verbose >= 3: + msg = _("Deleting '%s' because of too much.") % (oldfile) + self.logger.debug(msg) + result.append(oldfile) + continue + + # Now checking for maximum age + if maxage: + if age >= maxage: + if self.verbose >= 3: + msg = _("Deleting '%s' because of too old.") % (oldfile) + self.logger.debug(msg) + result.append(oldfile) + + if self.verbose > 3: + if len(result): + pp = pprint.PrettyPrinter(indent=4) + msg = _("Found logfiles to delete:") + "\n" + pp.pformat(result) + self.logger.debug(msg) + else: + msg = _("No old logfiles to delete found.") + self.logger.debug(msg) + return result + + #------------------------------------------------------------ + def _collect_old_logfiles(self, logfile, extension, compress_extension, cur_desc_index): + ''' + Collect all rotated versions of this logfile and gives back the + information about. + + @param logfile: the logfile to rotate + @type logfile: str + @param extension: additional fix file extension for rotated logfiles + @type extension: str + @param compress_extension: file extension for rotated and + compressed logfiles + @type compress_extension: str + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + + @return: all found old rotated logfiles as keys + and the last modification timestamp of these files as values + @rtype: dict + ''' + + definition = self.config[cur_desc_index] + _ = self.t.lgettext + + if self.verbose > 2: + msg = _("Retrieving all old logfiles for file '%s' ...") % (logfile) + self.logger.debug(msg) + + result = {} + + basename = os.path.basename(logfile) + dirname = os.path.dirname(logfile) + + if definition['dateext']: + basename += '.*' + + if definition['olddir']['dirname']: + # Create a file pattern depending on olddir definition + + olddir = definition['olddir']['dirname'] + + # Substitution of $dirname + olddir = re.sub(r'(?:\${dirname}|\$dirname(?![a-zA-Z0-9_]))', dirname, olddir) + + # Substitution of $basename + olddir = re.sub(r'(?:\${basename}|\$basename(?![a-zA-Z0-9_]))', basename, olddir) + + # Substitution of $nodename + olddir = re.sub(r'(?:\${nodename}|\$nodename(?![a-zA-Z0-9_]))', self.template['nodename'], olddir) + + # Substitution of $domain + olddir = re.sub(r'(?:\${domain}|\$domain(?![a-zA-Z0-9_]))', self.template['domain'], olddir) + + # Substitution of $machine + olddir = re.sub(r'(?:\${machine}|\$machine(?![a-zA-Z0-9_]))', self.template['machine'], olddir) + + # Substitution of $release + olddir = re.sub(r'(?:\${release}|\$release(?![a-zA-Z0-9_]))', self.template['release'], olddir) + + # Substitution of $sysname + olddir = re.sub(r'(?:\${sysname}|\$sysname(?![a-zA-Z0-9_]))', self.template['sysname'], olddir) + + if not os.path.isabs(olddir): + olddir = os.path.join(dirname, olddir) + olddir = os.path.normpath(olddir) + + #### + # Substituting all datetime.strftime() placeholders by shell pattern + + # weekday + olddir = re.sub(r'%[aA]', '*', olddir) + # name of month + olddir = re.sub(r'%[bBh]', '*', olddir) + # complete date + olddir = re.sub(r'%c', '*', olddir) + # century + olddir = re.sub(r'%C', '[0-9][0-9]', olddir) + # day of month + olddir = re.sub(r'%d', '[0-9][0-9]', olddir) + # date as %m/%d/%y + olddir = re.sub(r'%[Dx]', '[0-9][0-9]/[0-9][0-9]/[0-9][0-9]', olddir) + # Hour in 24-hours format + olddir = re.sub(r'%H', '[012][0-9]', olddir) + # Hour in 12-hours format + olddir = re.sub(r'%J', '[01][0-9]', olddir) + # number of month + olddir = re.sub(r'%m', '[01][0-9]', olddir) + # minute + olddir = re.sub(r'%M', '[0-5][0-9]', olddir) + # AM/PM + olddir = re.sub(r'%p', '[AP]M', olddir) + # complete time in 12-hours format with AM/PM + olddir = re.sub(r'%r', '[01][0-9]:[0-5][0-9]:[0-5][0-9] [AP]M', olddir) + # time in format %H:%M + olddir = re.sub(r'%R', '[012][0-9]:[0-5][0-9]', olddir) + # seconds + olddir = re.sub(r'%S', '[0-5][0-9]', olddir) + # complete time in 24-hours format + olddir = re.sub(r'%[TX]', '[012][0-9]:[0-5][0-9]:[0-5][0-9]', olddir) + # weekday as a number (0-7) + olddir = re.sub(r'%[uw]', '[0-7]', olddir) + # number of week in year (00-53) + olddir = re.sub(r'%[UVW]', '[0-5][0-9]', olddir) + # last two digits of the year + olddir = re.sub(r'%y', '[0-9][0-9]', olddir) + # year complete + olddir = re.sub(r'%Y', '[12][0-9][0-9][0-9]', olddir) + # time zone numeric + olddir = re.sub(r'%z', '[-+][0-9][0-9][0-9][0-9]', olddir) + # time zone name + olddir = re.sub(r'%Z', '*', olddir) + + dirname = olddir + + # composing file pattern + file_pattern = os.path.join(dirname, basename) + pattern_list = [] + pattern_list.append(file_pattern + extension) + pattern_list.append(file_pattern + '.[0-9]' + extension) + pattern_list.append(file_pattern + '.[0-9][0-9]' + extension) + pattern_list.append(file_pattern + '.[0-9][0-9][0-9]' + extension) + pattern_list.append(file_pattern + '.[0-9][0-9][0-9][0-9]' + extension) + pattern_list.append(file_pattern + '.[0-9][0-9][0-9][0-9][0-9]' + extension) + + if definition['compress']: + ext = extension + compress_extension + pattern_list.append(file_pattern + ext) + pattern_list.append(file_pattern + '.[0-9]' + ext) + pattern_list.append(file_pattern + '.[0-9][0-9]' + ext) + pattern_list.append(file_pattern + '.[0-9][0-9][0-9]' + ext) + pattern_list.append(file_pattern + '.[0-9][0-9][0-9][0-9]' + ext) + pattern_list.append(file_pattern + '.[0-9][0-9][0-9][0-9][0-9]' + ext) + + for pattern in pattern_list: + if self.verbose > 2: + msg = _("Search for pattern '%s' ...") % (pattern) + self.logger.debug(msg) + found_files = glob.glob(pattern) + for oldfile in found_files: + oldfile = os.path.abspath(oldfile) + if oldfile == logfile: + continue + statinfo = os.stat(oldfile) + result[oldfile] = statinfo.st_mtime + + if self.verbose > 3: + pp = pprint.PrettyPrinter(indent=4) + msg = _("Found old logfiles:") + "\n" + pp.pformat(result) + self.logger.debug(msg) + return result + + #------------------------------------------------------------ + def _get_rotations(self, logfile, target, cur_desc_index): + ''' + Retrieves all files to move and to rotate and gives them back + as a dict. + + @param logfile: the logfile to rotate + @type logfile: str + @param target: name of the rotated logfile + @type target: str + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + + @return: dict in the form:: + { + 'compress_extension': '.gz', + 'extension': '', + 'rotate': { + 'from': , + 'to': + }, + 'move': [ + ... + { 'from': , 'to': , 'compressed': True}, + { 'from': , 'to': , 'compressed': True}, + { 'from': , 'to': , 'compressed': False}, + ], + } + + the order in the list 'move' is the order, how the + files have to rename. + @rtype: dict + ''' + + definition = self.config[cur_desc_index] + _ = self.t.lgettext + + if self.verbose > 2: + msg = _("Retrieving all movings and rotations for logfile '%(file)s' to target '%(target)s' ...") \ + % {'file': logfile, 'target': target} + self.logger.debug(msg) + + result = { 'rotate': {}, 'move': [] } + + # retrieve additional file extension of logfile after rotation + # without compress extension + extension = definition['extension'] + if extension is None: + extension = '' + match = re.search(r'^\s*$', extension) + if match: + extension = '' + if extension != '': + match = re.search(r'^\.', extension) + if not match: + extension = "." + extension + result['extension'] = extension + extension_wo_compress = extension + + # retrieve additional file extension of logfile after rotation + # for compress extension + compress_extension = '' + if definition['compress']: + compress_extension = definition['compressext'] + match = re.search(r'^\.', compress_extension) + if not match: + compress_extension = "." + compress_extension + result['compress_extension'] = compress_extension + + # appending a trailing '.0', if there are no other differences + # between logfile and target + i = definition['start'] + if i is None: + i = 0 + resulting_target = target + extension_wo_compress + target_wo_number = resulting_target + if resulting_target == logfile: + resulting_target = resulting_target + "." + str(i) + + result['rotate']['from'] = logfile + result['rotate']['to'] = resulting_target + + # resulting target exists, retrieve cyclic rotation + if os.path.exists(resulting_target): + if self.verbose > 3: + msg = _("Resulting target '%s' exists, retrieve cyclic rotation ...") \ + % (resulting_target) + self.logger.debug(msg) + target_wo_cext_old = target_wo_number + "." + str(i) + target_with_cext_old = target_wo_cext_old + compress_extension + while os.path.exists(target_wo_cext_old) or os.path.exists(target_with_cext_old): + i += 1 + target_wo_cext_new = target_wo_number + "." + str(i) + target_with_cext_new = target_wo_cext_new + compress_extension + if self.verbose > 4: + msg = _("Cyclic rotation from '%(from)s' to '%(to)s'.") \ + % {'from': target_wo_cext_old, 'to': target_wo_cext_new} + self.logger.debug(msg) + pair = { + 'from': target_wo_cext_old, + 'to': target_wo_cext_new, + 'compressed': False, + } + if definition['compress']: + if os.path.exists(target_with_cext_old): + pair['compressed'] = True + result['move'].insert(0, pair) + target_wo_cext_old = target_wo_cext_new + target_with_cext_old = target_with_cext_new + + if self.verbose > 3: + pp = pprint.PrettyPrinter(indent=4) + msg = _("Found rotations:") + "\n" + pp.pformat(result) + self.logger.debug(msg) + return result + + #------------------------------------------------------------ + def _get_rotation_target(self, logfile, cur_desc_index, olddir = None): + ''' + Retrieves the name of the rotated logfile and gives it back. + + @param logfile: the logfile to rotate + @type logfile: str + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + @param olddir: the directory of the rotated logfile + if None, store the rotated logfile + in their original directory + @type olddir: str or None + + @return: name of the rotated logfile + @rtype: str + ''' + + definition = self.config[cur_desc_index] + + _ = self.t.lgettext + + if self.verbose > 2: + msg = _("Retrieving the name of the rotated file of '%s' ...") % (logfile) + self.logger.debug(msg) + + target = logfile + if olddir is not None: + basename = os.path.basename(logfile) + target = os.path.join(olddir, basename) + + if definition['dateext']: + pattern = definition['datepattern'] + if pattern is None: + pattern = '%Y-%m-%d' + dateext = datetime.utcnow().strftime(pattern) + if self.verbose > 3: + msg = _("Using date extension '.%(ext)s' from pattern '%(pattern)s'.") \ + % {'ext': dateext, 'pattern': pattern} + self.logger.debug(msg) + target += "." + dateext + + if self.verbose > 1: + msg = _("Using '%(target)s' as target for rotation of logfile '%(logfile)s'.") \ + % {'target': target, 'logfile': logfile} + self.logger.debug(msg) + return target + + #------------------------------------------------------------ + def _create_olddir(self, logfile, cur_desc_index): + ''' + Creating the olddir, if necessary. + + @param logfile: the logfile to rotate + @type logfile: str + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + + @return: Name of the retrieved olddir, ".", if storing + the rotated logfiles in their original directory or + None in case of some minor errors (olddir couldn't + created a.s.o.) + @rtype: str or None + ''' + + definition = self.config[cur_desc_index] + + _ = self.t.lgettext + + uid = os.geteuid() + gid = os.getegid() + + o = definition['olddir'] + if not o['dirname']: + if self.verbose > 1: + msg = _("No dirname directive for olddir given.") + self.logger.debug(msg) + return "." + olddir = o['dirname'] + + mode = o['mode'] + if mode is None: + mode = int('0755', 8) + owner = o['owner'] + if not owner: + owner = uid + group = o['group'] + if not group: + group = gid + + basename = os.path.basename(logfile) + dirname = os.path.dirname(logfile) + + match = re.search(r'%', olddir) + if match: + o['dateformat'] = True + olddir = datetime.utcnow().strftime(olddir) + + # Substitution of $dirname + olddir = re.sub(r'(?:\${dirname}|\$dirname(?![a-zA-Z0-9_]))', dirname, olddir) + + # Substitution of $basename + olddir = re.sub(r'(?:\${basename}|\$basename(?![a-zA-Z0-9_]))', basename, olddir) + + # Substitution of $nodename + olddir = re.sub(r'(?:\${nodename}|\$nodename(?![a-zA-Z0-9_]))', self.template['nodename'], olddir) + + # Substitution of $domain + olddir = re.sub(r'(?:\${domain}|\$domain(?![a-zA-Z0-9_]))', self.template['domain'], olddir) + + # Substitution of $machine + olddir = re.sub(r'(?:\${machine}|\$machine(?![a-zA-Z0-9_]))', self.template['machine'], olddir) + + # Substitution of $release + olddir = re.sub(r'(?:\${release}|\$release(?![a-zA-Z0-9_]))', self.template['release'], olddir) + + # Substitution of $sysname + olddir = re.sub(r'(?:\${sysname}|\$sysname(?![a-zA-Z0-9_]))', self.template['sysname'], olddir) + + if not os.path.isabs(olddir): + olddir = os.path.join(dirname, olddir) + olddir = os.path.normpath(olddir) + + if self.verbose > 1: + msg = _("Olddir name is now '%s'") % (olddir) + self.logger.debug(msg) + + # Check for Existence and Consistence + if os.path.exists(olddir): + if os.path.isdir(olddir): + if os.access(olddir, (os.W_OK | os.X_OK)): + if self.verbose > 2: + msg = _("Olddir '%s' allready exists, not created.") % (olddir) + self.logger.debug(msg) + olddir = os.path.realpath(olddir) + return olddir + else: + msg = _("No write and execute access to olddir '%s'.") % (olddir) + if self.test: + self.logger.warning(msg) + return olddir + raise LogrotateHandlerError(msg) + return None + else: + msg = _("Olddir '%s' exists, but is not a valid directory.") % (olddir) + raise LogrotateHandlerError(msg) + return None + + dirs = [] + dir_head = olddir + while dir_head != os.sep: + (dir_head, dir_tail) = os.path.split(dir_head) + dirs.insert(0, dir_tail) + if self.verbose > 2: + msg = _("Directory chain to create: '%s'") % (str(dirs)) + self.logger.debug(msg) + + # Create olddir recursive, if necessary + msg = _("Creating olddir '%s' recursive ...") % (olddir) + self.logger.info(msg) + create_dir = None + parent_statinfo = os.stat(os.sep) + parent_mode = parent_statinfo.st_mode + parent_uid = parent_statinfo.st_uid + parent_gid = parent_statinfo.st_gid + while len(dirs): + dir_head = dirs.pop(0) + if create_dir: + create_dir = os.path.join(create_dir, dir_head) + else: + create_dir = os.sep + dir_head + if self.verbose > 3: + msg = _("Try to create directory '%s' ...") % (create_dir) + self.logger.debug(msg) + if os.path.exists(create_dir): + if os.path.isdir(create_dir): + if self.verbose > 3: + msg = _("Directory '%s' allready exists, not created.") % (create_dir) + self.logger.debug(msg) + parent_statinfo = os.stat(create_dir) + parent_mode = parent_statinfo.st_mode + parent_uid = parent_statinfo.st_uid + parent_gid = parent_statinfo.st_gid + continue + else: + msg = _("Directory '%s' exists, but is not a valid directory.") % (create_dir) + self.logger.error(msg) + return None + msg = _("Creating directory '%s' ...") % (create_dir) + self.logger.debug(msg) + create_mode = parent_mode + if o['mode'] is not None: + create_mode = o['mode'] + create_uid = parent_uid + if o['owner'] is not None: + create_uid = o['owner'] + create_gid = parent_gid + if o['group'] is not None: + create_gid = o['group'] + if self.verbose > 1: + msg = _("Create permissions: %(mode)4o, Owner-UID: %(uid)d, Group-GID: %(gid)d") \ + % {'mode': create_mode, 'uid': create_uid, 'gid': create_gid} + self.logger.debug(msg) + if not self.test: + if self.verbose > 2: + msg = "os.mkdir('%s', %4o)" % (create_dir, create_mode) + self.logger.debug(msg) + try: + os.mkdir(create_dir, create_mode) + except OSError, e: + msg = _("Error on creating directory '%(dir)s': %(err)s") \ + % {'dir': create_dir, 'err': e.strerror} + self.logger.error(msg) + return None + if (create_uid != uid) or (create_gid != gid): + myuid = os.geteuid() + if myuid != 0: + msg = _("Only root may execute chown().") + if self.test: + self.logger.info(msg) + else: + self.logger.warning(msg) + else: + if self.verbose > 2: + msg = "os.chown('%s', %d, %d)" % (create_dir, create_uid, create_gid) + self.logger.debug(msg) + try: + os.chown(create_dir, create_uid, create_gid) + except OSError, e: + msg = _("Error on chowning directory '%(dir)s': %(err)s") \ + % {'dir': create_dir, 'err': e.strerror} + self.logger.error(msg) + return None + + olddir = os.path.realpath(olddir) + return olddir + + #------------------------------------------------------------ + def _execute_command(self, command, force=False, expected_retcode=0): + ''' + Executes the given command as an OS command in a shell. + + @param command: the command to execute + @type command: str + @param force: force executing command even if self.test == True + @type force: bool + @param expected_retcode: expected returncode of the command + (should be 0) + @type expected_retcode: int + + @return: Success of the comand (shell returncode == 0) + @rtype: bool + ''' + + _ = self.t.lgettext + if self.verbose > 3: + msg = _("Executing command: '%s'") % (command) + self.logger.debug(msg) + if not force: + if self.test: + return True + try: + retcode = subprocess.call(command, shell=True) + if self.verbose > 3: + msg = _("Got returncode: '%s'") % (retcode) + self.logger.debug(msg) + if retcode < 0: + msg = _("Child was terminated by signal %d") % (-retcode) + self.logger.error(msg) + return False + if retcode != expected_retcode: + return False + return True + except OSError, e: + msg = _("Execution failed: %s") % (str(e)) + self.logger.error(msg) + return False + + return False + + #------------------------------------------------------------ + def _should_rotate(self, logfile, cur_desc_index): + ''' + Determines, whether a logfile should rotated dependend on + the informations in the definition. + + Throughs an LogrotateHandlerError on harder errors. + + @param logfile: the logfile to inspect + @type logfile: str + @param cur_desc_index: index of self.config for definition + of logfile from configuration file + @type cur_desc_index: int + + @return: to rotate or not + @rtype: bool + ''' + + definition = self.config[cur_desc_index] + + _ = self.t.lgettext + + if self.verbose > 2: + msg = _("Check, whether logfile '%s' should rotated.") % (logfile) + self.logger.debug(msg) + + if not os.path.exists(logfile): + msg = _("logfile '%s' doesn't exists, not rotated") % (logfile) + if not definition['missingok']: + self.logger.error(msg) + else: + if self.verbose > 1: + self.logger.debug(msg) + return False + + if not os.path.isfile(logfile): + msg = _("logfile '%s' is not a regular file, not rotated") % (logfile) + self.logger.warning(msg) + return False + + filesize = os.path.getsize(logfile) + if self.verbose > 2: + msg = _("Filesize of '%(file)s': %(size)d") % {'file': logfile, 'size': filesize} + self.logger.debug(msg) + + if not filesize: + if not definition['ifempty']: + if self.verbose > 1: + msg = _("Logfile '%s' has a filesize of Zero, not rotated") % (logfile) + self.logger.debug(msg) + return False + + if self.force: + if self.verbose > 1: + msg = _("Rotating of '%s' because of force mode.") % (logfile) + self.logger.debug(msg) + return True + + maxsize = definition['size'] + if maxsize is None: + maxsize = 0 + + last_rotated = self.state_file.get_rotation_date(logfile) + if self.verbose > 2: + msg = _("Date of last rotation: %s") %(last_rotated.isoformat(' ')) + self.logger.debug(msg) + next_rotation = last_rotated + timedelta(days = definition['period']) + if self.verbose > 2: + msg = _("Date of next rotation: %s") %(next_rotation.isoformat(' ')) + self.logger.debug(msg) + + if filesize < maxsize: + if self.verbose > 1: + msg = _("Filesize %(filesize)d is less than %(maxsize)d, rotation not necessary.") \ + % {'filesize': filesize, 'maxsize': maxsize} + self.logger.debug(msg) + return False + + curdate = datetime.utcnow().replace(tzinfo = utc) + if next_rotation > curdate: + if self.verbose > 1: + msg = _("Date of next rotation '%(next)s' is in future, rotation not necessary.") \ + % {'next': next_rotation.isoformat(' ')} + self.logger.debug(msg) + return False + + return True + + #------------------------------------------------------------ + def delete_oldfiles(self): + ''' + Deleting of all logfiles in self.files_delete + + @return: None + ''' + + _ = self.t.lgettext + + msg = _("Deletion of all superfluid logfiles ...") + self.logger.debug(msg) + + if not len(self.files_delete.keys()): + msg = _("No logfiles to delete found.") + self.logger.info(msg) + + for logfile in sorted(self.files_delete.keys(), key=str.lower): + msg = _("Deleting file '%s' ...") % (logfile) + self.logger.info(msg) + if not self.test: + try: + os.remove(logfile) + except OSError, e: + msg = _("Error on removing file '%(file)s': %(err)s") \ + % {'file': logfile, 'err': e.strerror} + self.logger.error(msg) + + return + + #------------------------------------------------------------ + def compress(self): + ''' + Compressing all logfiles in self.files_compress + + @return: None + ''' + + _ = self.t.lgettext + + msg = _("Compression of all uncompressed logfiles ...") + self.logger.debug(msg) + + if not len(self.files_compress.keys()): + msg = _("No logfiles to compress found.") + self.logger.info(msg) + + for logfile in sorted(self.files_compress.keys(), key=str.lower): + + cur_desc_index = self.files_compress[logfile] + definition = self.config[cur_desc_index] + command = definition['compresscmd'] + compress_extension = definition['compressext'] + compress_opts = definition['compressoptions'] + + match = re.search(r'^\.', compress_extension) + if not match: + compress_extension = "." + compress_extension + target = logfile + compress_extension + + # Check existence source logfile + if not os.path.exists(logfile): + msg = _("Source file '%s' for compression doesn't exists.") % (logfile) + raise LogrotateHandlerError(msg) + return + + # Check existence target (compressed file) + if os.path.exists(target): + if os.path.samefile(logfile, target): + msg = _("Source file '%(source)s' and target file '%(target)s' are the same file.") \ + % {'source': logfile, 'target': target} + raise LogrotateHandlerError(msg) + return + msg = _("Target file '%s' for compression allready exists.") %(target) + self.logger.warning(msg) + + # Check for filesize Zero => not compressed + filesize = os.path.getsize(logfile) + if filesize <= 0: + msg = _("File '%s' has a size of 0, skip compressing.") % (logfile) + self.logger.info(msg) + continue + + # Execute compressing ... + msg = _("Compressing file '%(file)s' to '%(target)s' with '%(cmd)s' ...") \ + % {'file': logfile, 'target': target, 'cmd': command} + self.logger.info(msg) + + if command == 'internal_gzip': + self._compress_internal_gzip(logfile, target) + elif command == 'internal_bzip2': + self._compress_internal_bzip2(logfile, target) + elif command == 'internal_zip': + self._compress_internal_zip(logfile, target) + else: + self._compress_external(logfile, target, command, compress_opts) + + return + + #------------------------------------------------------------ + def _compress_external(self, source, target, command, options): + ''' + Compression of the given source file to the target file + with an external command. + + It raises a LogrotateHandlerError on uncoverable errors. + + @param source: the source file to compress + @type source: str + @param target: the filename of the compressed file. + @type target: str + @param command: the OS command to use to compress + @type command: str + @param options: additional options to the compress command + possible placeholders inside the options: + - {}: placeholder for sourcefile + - []: placeholder for targetfile + @type options: str + + @return: success or not + @rtype: bool + ''' + + _ = self.t.lgettext + + if self.verbose > 1: + msg = _("Compressing source '%(source)s' to target'%(target)s' with command '%(cmd)s'.") \ + % {'source': source, 'target': target, 'cmd': command} + self.logger.debug(msg) + + if options is None: + options = '' + + # substituting [] in compressoptions with qouted target file name + match = re.search(r'\[\]', options) + if match: + if self.verbose > 3: + msg = _("Substituting '[]' in compressoptions with '%s'.") % ('"' + target + '"') + self.logger.debug(msg) + options = re.sub(r'\[\]', '"' + target + '"', options) + + # substituting or trailing command with quoted source file name + match = re.search(r'\{\}', options) + if match: + if self.verbose > 3: + msg = _("Substituting '{}' in compressoptions with '%s'.") % ('"' + source + '"') + self.logger.debug(msg) + options = re.sub(r'\{\}', '"' + source + '"', options) + else: + options += ' "' + source + '"' + + if self.verbose > 2: + msg = _("Compress options: '%s'.") % (options) + self.logger.debug(msg) + + cmd = command + ' ' + options + + src_statinfo = os.stat(source) + + if not self._execute_command(cmd): + return False + + if not self.test: + if not os.path.exists(target): + msg = _("Target '%s' of compression doesn't exists after executing compression command.") \ + % (target) + self.logger.error(msg) + return False + + if os.path.exists(source): + + self._copy_file_metadata(source=source, target=target) + + # And last, but not least, delete uncompressed file + if self.verbose > 1: + msg = _("Deleting uncompressed file '%s' ...") % (source) + self.logger.debug(msg) + + if not self.test: + try: + os.remove(source) + except OSError, e: + msg = _("Error removing uncompressed file '%(file)s': %(msg)") \ + % {'file': source, 'msg': str(e) } + self.logger.error(msg) + return False + + else: + + self._copy_file_metadata(target=target, statinfo=src_statinfo) + + return True + #------------------------------------------------------------ + def _copy_file_metadata(self, target, source=None, statinfo=None): + ''' + Copy all metadata (owner, permissions, timestamps a.s.o) from + a source file onto a target file. + The target file must be exists. + Either an existing source file (parameter 'source') or the + statinfo of a former existing file (parameter 'statinfo') must + be given. + + It raises a LogrotateHandlerError on uncoverable errors. + + @param target: filename of an existing target file or directory + @type target: str + @param source: filename of an existing source file or directory + or None, if statinfo was given, + has precedence before a given statinfo + @type source: str or None + @param statinfo: stat object from os.stat() or None, if source was given + @type statinfo: stat-object or None + + @return: success or not + @rtype: bool + ''' + + _ = self.t.lgettext + + if source is None and statinfo is None: + msg = _("Neither 'target' nor 'statinfo' was given on calling _copy_file_metadata().") + raise LogrotateHandlerError(msg) + return False + + if not os.path.exists(target): + msg = _("File or directory '%s' doesn't exists.") % (target) + if self.test: + self.logger.info(msg) + return True + self.logger.error(msg) + return False + + new_statinfo = statinfo + old_statinfo = os.stat(target) + + msg = _("Copying all file metadata to target '%s' ...") % (target) + self.logger.info(msg) + + if source is not None: + + # a source object was given + + if not os.path.exists(source): + msg = _("File or directory '%s' doesn't exists.") % (source) + self.logger.error(msg) + return False + + new_statinfo = os.stat(source) + + # Copying permissions and timestamps from source to target + if self.verbose > 1: + msg = _("Copying permissions and timestamps from source '%(src)s' to target '%(target)s'.") \ + % {'src': source, 'target': target} + self.logger.debug(msg) + if not self.test: + shutil.copystat(source, target) + + else: + + # a source statinfo was given + + atime = new_statinfo.st_atime + mtime = new_statinfo.st_mtime + mode = new_statinfo.st_mode + + # Setting atime and mtime + if self.verbose > 1: + msg = _("Setting atime and mtime of target '%s'.") % (target) + self.logger.debug(msg) + if not self.test: + try: + os.utime(target, (atime, mtime)) + except OSError, e: + msg = _("Error on setting times on target file '%(target)s': %(err)s") \ + % {'target': target, 'err': e.strerror} + self.logger.warning(msg) + return False + + # Setting permissions + old_mode = old_statinfo.st_mode + if mode != old_mode: + if self.verbose > 1: + msg = _("Setting permissions of '%(target)s' to %(mode)4o.") \ + % {'target': target, 'mode': new_mode} + self.logger.info(msg) + if not self.test: + try: + os.chmod(target, mode) + except OSError, e: + msg = _("Error on chmod of '%(target)s': %(err)s") \ + % {'target': target, 'err': e.strerror} + self.logger.warning(msg) + return False + + # Copying ownership from source to target + new_uid = new_statinfo.st_uid + new_gid = new_statinfo.st_gid + old_uid = old_statinfo.st_uid + old_gid = old_statinfo.st_gid + + if (old_uid != new_uid) or (old_gid != new_gid): + if self.verbose > 1: + msg = _("Copying ownership from source to target.") + self.logger.debug(msg) + myuid = os.geteuid() + if myuid != 0: + msg = _("Only root may execute chown().") + if self.test: + self.logger.info(msg) + return True + else: + self.logger.warning(msg) + return False + if not self.test: + try: + os.chown(target, old_uid, old_gid) + except OSError, e: + msg = _("Error on chown of '%(file)s': %(err)s") \ + % {'file': target, 'err': e.strerror} + self.logger.warning(msg) + return False + + return True + + #------------------------------------------------------------ + def _compress_internal_zip(self, source, target): + ''' + Compression of the given source file to the target file + with the Python module zipfile. + + It raises a LogrotateHandlerError on some errors. + + @param source: the source file to compress + @type source: str + @param target: the filename of the compressed file. + @type target: str + + @return: success or not + @rtype: bool + ''' + + _ = self.t.lgettext + + if self.verbose > 1: + msg = _("Compressing source '%(source)s' to target'%(target)s' with module zipfile.") \ + % {'source': source, 'target': target} + self.logger.debug(msg) + + if not self.test: + + # open target for writing + f_out = None + try: + f_out = zipfile.ZipFile( + file=target, + mode='w', + compression=zipfile.ZIP_DEFLATED + ) + except IOError, e: + msg = _("Error on open file '%(file)s' on writing: %(err)s") \ + % {'file': target, 'err': str(e)} + self.logger.error(msg) + return False + + basename = os.path.basename(source) + f_out.write(source, basename) + f_out.close() + + self._copy_file_metadata(source=source, target=target) + + # And last, but not least, delete uncompressed file + if self.verbose > 1: + msg = _("Deleting uncompressed file '%s' ...") % (source) + self.logger.debug(msg) + + if not self.test: + try: + os.remove(source) + except OSError, e: + msg = _("Error removing uncompressed file '%(file)s': %(msg)") \ + % {'file': source, 'msg': str(e) } + self.logger.error(msg) + return False + + return True + + #------------------------------------------------------------ + def _compress_internal_gzip(self, source, target): + ''' + Compression of the given source file to the target file + with the Python module gzip. + As compression level is allways used 9 (highest compression). + + It raises a LogrotateHandlerError on some errors. + + @param source: the source file to compress + @type source: str + @param target: the filename of the compressed file. + @type target: str + + @return: success or not + @rtype: bool + ''' + + _ = self.t.lgettext + + if self.verbose > 1: + msg = _("Compressing source '%(source)s' to target'%(target)s' with module gzip.") \ + % {'source': source, 'target': target} + self.logger.debug(msg) + + if not self.test: + # open source for reading + f_in = None + try: + f_in = open(source, 'rb') + except IOError, e: + msg = _("Error on open file '%(file)s' on reading: %(err)s") \ + % {'file': source, 'err': str(e)} + self.logger.error(msg) + return False + + # open target for writing + f_out = None + try: + f_out = gzip.open(target, 'wb') + except IOError, e: + msg = _("Error on open file '%(file)s' on writing: %(err)s") \ + % {'file': target, 'err': str(e)} + self.logger.error(msg) + f_in.close() + return False + + # compress and write target + f_out.writelines(f_in) + # close both files + f_out.close() + f_in.close() + + self._copy_file_metadata(source=source, target=target) + + # And last, but not least, delete uncompressed file + if self.verbose > 1: + msg = _("Deleting uncompressed file '%s' ...") % (source) + self.logger.debug(msg) + + if not self.test: + try: + os.remove(source) + except OSError, e: + msg = _("Error removing uncompressed file '%(file)s': %(msg)") \ + % {'file': source, 'msg': str(e) } + self.logger.error(msg) + return False + + return True + + #------------------------------------------------------------ + def _compress_internal_bzip2(self, source, target): + ''' + Compression of the given source file to the target file + with the Python module bz2. + As compression level is allways used 9 (highest compression). + + It raises a LogrotateHandlerError on some errors. + + @param source: the source file to compress + @type source: str + @param target: the filename of the compressed file. + @type target: str + + @return: success or not + @rtype: bool + ''' + + _ = self.t.lgettext + + if self.verbose > 1: + msg = _("Compressing source '%(source)s' to target'%(target)s' with module bz2.") \ + % {'source': source, 'target': target} + self.logger.debug(msg) + + if not self.test: + # open source for reading + f_in = None + try: + f_in = open(source, 'rb') + except IOError, e: + msg = _("Error on open file '%(file)s' on reading: %(err)s") \ + % {'file': source, 'err': str(e)} + self.logger.error(msg) + return False + + # open target for writing + f_out = None + try: + f_out = bz2.BZ2File(target, 'w') + except IOError, e: + msg = _("Error on open file '%(file)s' on writing: %(err)s") \ + % {'file': target, 'err': str(e)} + self.logger.error(msg) + f_in.close() + return False + + # compress and write target + f_out.writelines(f_in) + # close both files + f_out.close() + f_in.close() + + self._copy_file_metadata(source=source, target=target) + + # And last, but not least, delete uncompressed file + if self.verbose > 1: + msg = _("Deleting uncompressed file '%s' ...") % (source) + self.logger.debug(msg) + + if not self.test: + try: + os.remove(source) + except OSError, e: + msg = _("Error removing uncompressed file '%(file)s': %(msg)") \ + % {'file': source, 'msg': str(e) } + self.logger.error(msg) + return False + + return True + + + #------------------------------------------------------------ + def send_logfiles(self): + ''' + Sending all mails, they should be sent, to their recipients. + ''' + + _ = self.t.lgettext + + if self.verbose > 1: + pp = pprint.PrettyPrinter(indent=4) + msg = _("Struct files2send:") + "\n" + pp.pformat(self.files2send) + self.logger.debug(msg) + + for filename in self.files2send.keys(): + self.mailer.send_file(filename, self.files2send[filename][0], self.files2send[filename][1]) + + return + +#======================================================================== + +if __name__ == "__main__": + pass + + +#======================================================================== + +# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotate/LogRotateMailer.py b/LogRotate/LogRotateMailer.py new file mode 100755 index 0000000..9ff2628 --- /dev/null +++ b/LogRotate/LogRotateMailer.py @@ -0,0 +1,574 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# $Id$ +# $URL$ + +''' +@author: Frank Brehm +@contact: frank@brehm-online.com +@license: GPL3 +@copyright: (c) 2010-2011 by Frank Brehm, Berlin +@version: 0.0.2 +@summary: module for a logrotate mailer object to send + rotated logfiles per mail to a reciepient +''' + +import re +import logging +import pprint +import gettext +import os +import os.path +import sys +import pwd +import socket +import csv + +from datetime import datetime + +import mimetypes +import email.utils +from email import encoders +from email.message import Message +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.nonmultipart import MIMENonMultipart +from email.mime.text import MIMEText + +from quopri import encodestring as _encodestring + +from LogRotateCommon import email_valid + +revision = '$Revision$' +revision = re.sub( r'\$', '', revision ) +revision = re.sub( r'Revision: ', r'r', revision ) +revision = re.sub( r'\s*$', '', revision ) + +__author__ = 'Frank Brehm' +__copyright__ = '(C) 2011 by Frank Brehm, Berlin' +__contact__ = 'frank@brehm-online.com' +__version__ = '0.1.0 ' + revision +__license__ = 'GPL3' + +#======================================================================== + +class LogRotateMailerError(Exception): + ''' + Base class for exceptions in this module. + ''' + +#======================================================================== + +class LogRotateMailer(object): + ''' + Class for a mailer object to send + rotated logfiles per mail to a reciepient + + @author: Frank Brehm + @contact: frank@brehm-online.com + ''' + + #------------------------------------------------------- + def __init__( self, local_dir = None, + verbose = 0, + test_mode = False, + mailer_version = None, + ): + ''' + Constructor. + + @param local_dir: The directory, where the i18n-files (*.mo) + are located. If None, then system default + (/usr/share/locale) is used. + @type local_dir: str or None + @param verbose: verbosity (debug) level + @type verbose: int + @param test_mode: test mode - no write actions are made + @type test_mode: bool + @param mailer_version: version of the X-Mailer tag in the mail header + @type mailer_version: str + + @return: None + ''' + + self.t = gettext.translation( + 'LogRotateMailer', + local_dir, + fallback = True + ) + ''' + @ivar: a gettext translation object + @type: gettext.translation + ''' + + _ = self.t.lgettext + + self.verbose = verbose + ''' + @ivar: verbosity level (0 - 9) + @type: int + ''' + + self.test_mode = test_mode + ''' + @ivar: test mode - no write actions are made + @type: bool + ''' + + self.logger = logging.getLogger('pylogrotate.mailer') + ''' + @ivar: logger object + @type: logging.getLogger + ''' + + self._sendmail = None + ''' + @ivar: file name of the sendmail executable + ('/usr/sbin/sendmail' or '/usr/lib/sendmail') + used for sending the mails. + if None, the mails will sended via SMTP + @type: str or None + ''' + self._init_sendmail() + + self._from_address = ('me', 'info@uhu-banane.de') + ''' + @ivar: Mailaddress of the sender, tuple with the real name of + the sender and his mail address as the second value + @type: tuple + ''' + self._init_from_address() + + self._smtp_host = 'localhost' + ''' + @ivar: the hostname to use for SMTP (smarthost), if no + sendmail binary was found + @type: str + ''' + + self._smtp_port = 25 + ''' + @ivar: the port to use for SMTP to the smarthost + @type: int + ''' + + self._smtp_tls = False + ''' + @ivar: use TLS for sending via SMTP to smarthost + @type: bool + ''' + + self.smtp_user = None + ''' + @ivar: Authentication username for SMTP + @type: str or None + ''' + + self.smtp_passwd = None + ''' + @ivar: Authentication password for SMTP + @type: str or None + ''' + + self.mailer_version = __version__ + ''' + @ivar: version of the X-Mailer tag in the mail header + @type: str + ''' + if mailer_version is not None: + self.mailer_version = mailer_version + + #------------------------------------------------------------ + # Defintion of some properties + + #------------------------------------------------------------ + # Property 'from' + def _get_from_address(self): + ''' + Getter method for property 'from_address' + ''' + return email.utils.formataddr(self._from_address) + + def _set_from_address(self, value): + ''' + Setter method for property 'from_address' + ''' + _ = self.t.lgettext + if value is None: + msg = _("The 'From' address may not set to None.") + raise LogRotateMailerError(msg) + pair = ('', '') + if isinstance(value, tuple): + if len(value) < 2: + pair = email.utils.parseaddr(value[0]) + else: + pair = (value[0], value[1]) + else: + pair = email.utils.parseaddr(value) + + if ( (pair[0] is None or pair[0] == '') and + (pair[1] is None or pair[1] == '') ): + msg = _("Invalid mail address given: '%s'.") % (str(value)) + raise LogRotateMailerError(msg) + + if not email_valid(pair[1]): + msg = _("Invalid mail address given: '%s'.") % (str(value)) + raise LogRotateMailerError(msg) + + self._from_address = pair + if self.verbose > 3: + addr = email.utils.formataddr(pair) + msg = _("Set sender mail address to: '%s'.") % (addr) + self.logger.debug(msg) + + def _del_from_address(self): + ''' + Deleter method for property 'from_address' + ''' + self._init_from_address() + + from_address = property(_get_from_address, _set_from_address, _del_from_address, "The mail address of the sender") + + #------------------------------------------------------------ + # Property 'sendmail' + def _get_sendmail(self): + ''' + Getter method for property 'sendmail' + ''' + return self._sendmail + + def _set_sendmail(self, value): + ''' + Setter method for property 'sendmail' + ''' + _ = self.t.lgettext + if value is None or value == '': + self._sendmail = None + return + + if os.path.isabs(value): + if os.path.exists(value): + cmd = os.path.normpath(value) + if os.access(cmd, os.X_OK): + msg = _("Using '%s' as the sendmail command.") % (cmd) + self.logger.debug(msg) + self._sendmail = cmd + return + else: + msg = _("No execute permissions to '%s'.") % (cmd) + self.logger.warning(msg) + return + else: + msg = _("Sendmail command '%s' not found.") % (value) + self.logger.warning(msg) + return + else: + msg = _("Only absolute path allowed for a sendmail command: '%s'.") % (value) + self.logger.warning(msg) + return + + def _del_sendmail(self): + ''' + Deleter method for property 'from_address' + ''' + self._sendmail = None + + sendmail = property(_get_sendmail, _set_sendmail, _del_sendmail, "The sendmail executable for sending mails local") + + #------------------------------------------------------------ + # Property 'smtp_host' + def _get_smtp_host(self): + ''' + Getter method for property 'smtp_host' + ''' + return self._smtp_host + + def _set_smtp_host(self, value): + ''' + Setter method for property 'smtp_host' + ''' + _ = self.t.lgettext + if value: + self._smtp_host = value + + smtp_host = property(_get_smtp_host, _set_smtp_host, None, "The hostname to use for sending mails via SMTP (smarthost)") + + #------------------------------------------------------------ + # Property 'smtp_port' + def _get_smtp_port(self): + ''' + Getter method for property 'smtp_port' + ''' + return self._smtp_port + + def _set_smtp_port(self, value): + ''' + Setter method for property 'smtp_port' + ''' + _ = self.t.lgettext + if value: + port = 25 + try: + port = int(value) + except ValueError, e: + return + if port < 1 or port >= 2**15: + return + self._smtp_port = port + + smtp_port = property(_get_smtp_port, _set_smtp_port, None, "The port to use for sending mails via SMTP") + + #------------------------------------------------------------ + # Property 'smtp_tls' + def _get_smtp_tls(self): + ''' + Getter method for property 'smtp_tls' + ''' + return self._smtp_tls + + def _set_smtp_tls(self, value): + ''' + Setter method for property 'smtp_tls' + ''' + self._smtp_tls = bool(value) + + smtp_tls = property(_get_smtp_tls, _set_smtp_tls, None, "Use TLS for sending mails via SMTP (smarthost)") + + #------------------------------------------------------------ + # Other Methods + + #------------------------------------------------------- + def __del__(self): + ''' + Destructor. + ''' + + _ = self.t.lgettext + if self.verbose > 2: + msg = _("Mailer object will destroyed.") + self.logger.debug(msg) + + #------------------------------------------------------------ + def __str__(self): + ''' + Typecasting function for translating object structure + into a string + + @return: structure as string + @rtype: str + ''' + + pp = pprint.PrettyPrinter(indent=4) + structure = self.as_dict() + return pp.pformat(structure) + + #------------------------------------------------------- + def as_dict(self): + ''' + Transforms the elements of the object into a dict + + @return: structure as dict + @rtype: dict + ''' + + res = {} + res['t'] = self.t + res['verbose'] = self.verbose + res['test_mode'] = self.test_mode + res['logger'] = self.logger + res['sendmail'] = self.sendmail + res['from'] = self.from_address + res['smtp_host'] = self.smtp_host + res['smtp_port'] = self.smtp_port + res['smtp_tls'] = self.smtp_tls + res['smtp_user'] = self.smtp_user + res['smtp_passwd'] = self.smtp_passwd + res['mailer_version'] = self.mailer_version + + return res + + #------------------------------------------------------- + def _init_from_address(self): + ''' + Initialises the sender mail address + ''' + + _ = self.t.lgettext + + cur_user = pwd.getpwuid(os.getuid())[0] + cur_host = socket.getfqdn() + addr = cur_user + '@' + cur_host + + if self.verbose > 3: + msg = _("Using <%s> as the sender mail address.") % (addr) + self.logger.debug(msg) + + self._from_address = (None, addr) + + #------------------------------------------------------- + def _init_sendmail(self): + ''' + Initialises the sendmail with + ''' + + _ = self.t.lgettext + + progs = [ + os.sep + os.path.join('usr', 'sbin', 'sendmail'), + os.sep + os.path.join('usr', 'lib', 'sendmail'), + ] + + if self.verbose > 3: + msg = _("Initial search for the sendmail executable ...") + self.logger.debug(msg) + + for prog in progs: + + if self.verbose > 3: + msg = _("Testing for '%s' ...") % (prog) + self.logger.debug(msg) + + if os.path.exists(prog): + if os.access(prog, os.X_OK): + if self.verbose > 1: + msg = _("Using '%s' as the sendmail command.") % (prog) + self.logger.debug(msg) + self._sendmail = prog + break + else: + msg = _("No execute permissions to '%s'.") % (prog) + self.logger.warning(msg) + + return + + #------------------------------------------------------- + def send_file(self, + filename, + addresses, + original=None, + mime_type='text/plain', + rotate_date=None, + charset=None + ): + ''' + Mails the file with the given file name as an attachement + to the given recipient(s). + + Raises a LogRotateMailerError on harder errors. + + @param filename: The file name of the file to send (the existing, + rotated and maybe compressed logfile). + @type filename: str + @param addresses: A list of tuples of a pair in the form + of the return value of email.utils.parseaddr() + @type addresses: list + @param original: The file name of the original (unrotated) logfile for + informational purposes. + If not given, filename is used instead. + @type original: str or None + @param mime_type: MIME type (content type) of the original logfile, + defaults to 'text/plain' + @type mime_type: str + @param rotate_date: datetime object of rotation, defaults to now() + @type rotate_date: datetime or None + @param charset: character set of (uncompreesed) logfile, if the + mime_type is 'text/plain', defaults to 'utf-8' + @type charset: str or None + + @return: success of sending + @rtype: bool + ''' + + _ = self.t.lgettext + + if not os.path.exists(filename): + msg = _("File '%s' dosn't exists.") % (filename) + self.logger.error(msg) + return False + + if not os.path.isfile(filename): + msg = _("File '%s' is not a regular file.") % (filename) + self.logger.warning(msg) + return False + + basename = os.path.basename(filename) + if not original: + original = os.path.abspath(filename) + + if not rotate_date: + rotate_date = datetime.now() + + msg = _("Sending mail with attached file '%(file)s' to: %(rcpt)s") \ + % {'file': basename, + 'rcpt': ', '.join(map(lambda x: '"' + email.utils.formataddr(x) + '"', addresses))} + self.logger.debug(msg) + + mail_container = MIMEMultipart() + mail_container['Subject'] = ( "Rotated logfile '%s'" % (filename) ) + mail_container['X-Mailer'] = ( "pylogrotate version %s" % (self.mailer_version) ) + mail_container['From'] = self.from_address + mail_container['To'] = ', '.join(map(lambda x: email.utils.formataddr(x), addresses)) + mail_container.preamble = 'You will not see this in a MIME-aware mail reader.\n' + + # Generate Text of the first part of mail body + mailtext = "Rotated Logfile:\n\n" + mailtext += "\t - " + filename + "\n" + mailtext += "\t (" + original + ")\n" + mailtext += "\n" + mailtext += "Date of rotation: " + rotate_date.isoformat(' ') + mailtext += "\n" + mailtext = _encodestring(mailtext, quotetabs=False) + mail_part = MIMENonMultipart('text', 'plain', charset=sys.getdefaultencoding()) + mail_part.set_payload(mailtext) + mail_part['Content-Transfer-Encoding'] = 'quoted-printable' + mail_container.attach(mail_part) + + ctype, encoding = mimetypes.guess_type(filename) + if self.verbose > 3: + msg = _("Guessed content-type: '%(ctype)s' and encoding '%(encoding)s'.") \ + % {'ctype': ctype, 'encoding': encoding } + self.logger.debug(msg) + + if encoding: + if encoding == 'gzip': + ctype = 'application/x-gzip' + elif encoding == 'bzip2': + ctype = 'application/x-bzip2' + else: + ctype = 'application/octet-stream' + + if not ctype: + ctype = mime_type + + maintype, subtype = ctype.split('/', 1) + fp = open(filename, 'rb') + mail_part = MIMEBase(maintype, subtype) + mail_part.set_payload(fp.read()) + fp.close() + if maintype == 'text': + msgtext = mail_part.get_payload() + msgtext = _encodestring(msgtext, quotetabs=False) + mail_part.set_payload(msgtext) + mail_part['Content-Transfer-Encoding'] = 'quoted-printable' + else: + encoders.encode_base64(mail_part) + mail_part.add_header('Content-Disposition', 'attachment', filename=basename) + mail_container.attach(mail_part) + + composed = mail_container.as_string() + if self.verbose > 4: + msg = _("Generated E-mail:") + "\n" + composed + self.logger.debug(msg) + + return True + +#======================================================================== + +if __name__ == "__main__": + pass + + +#======================================================================== + +# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotate/LogRotateScript.py b/LogRotate/LogRotateScript.py new file mode 100755 index 0000000..34268ca --- /dev/null +++ b/LogRotate/LogRotateScript.py @@ -0,0 +1,550 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# $Id$ +# $URL$ + +''' +@author: Frank Brehm +@contact: frank@brehm-online.com +@license: GPL3 +@copyright: (c) 2010-2011 by Frank Brehm, Berlin +@version: 0.0.2 +@summary: module for a logrotate script object + (for pre- and postrotate actions) +''' +import re +import logging +import subprocess +import pprint +import gettext + +revision = '$Revision$' +revision = re.sub( r'\$', '', revision ) +revision = re.sub( r'Revision: ', r'r', revision ) +revision = re.sub( r'\s*$', '', revision ) + +__author__ = 'Frank Brehm' +__copyright__ = '(C) 2011 by Frank Brehm, Berlin' +__contact__ = 'frank@brehm-online.com' +__version__ = '0.1.0 ' + revision +__license__ = 'GPL3' + +#======================================================================== + +class LogRotateScriptError(Exception): + ''' + Base class for exceptions in this module. + ''' + +#======================================================================== + +class LogRotateScript(object): + ''' + Class for encapsulating a logrotate script + (for pre- and postrotate actions) + + @author: Frank Brehm + @contact: frank@brehm-online.com + ''' + + #------------------------------------------------------- + def __init__( self, name, + local_dir = None, + verbose = 0, + test_mode = False, + ): + ''' + Constructor. + + @param name: the name of the script as an identifier + @type name: str + @param local_dir: The directory, where the i18n-files (*.mo) + are located. If None, then system default + (/usr/share/locale) is used. + @type local_dir: str or None + @param verbose: verbosity (debug) level + @type verbose: int + @param test_mode: test mode - no write actions are made + @type test_mode: bool + + @return: None + ''' + + self.t = gettext.translation( + 'LogRotateScript', + local_dir, + fallback = True + ) + ''' + @ivar: a gettext translation object + @type: gettext.translation + ''' + + _ = self.t.lgettext + + self.verbose = verbose + ''' + @ivar: verbosity level (0 - 9) + @type: int + ''' + + self._name = name + ''' + @ivar: the name of the script as an identifier + @type: str + ''' + + self.test_mode = test_mode + ''' + @ivar: test mode - no write actions are made + @type: bool + ''' + + self.logger = logging.getLogger('pylogrotate.script') + ''' + @ivar: logger object + @type: logging.getLogger + ''' + + self._cmd = [] + ''' + @ivar: List of commands to execute + @type: list + ''' + + self._post_files = 0 + ''' + @ivar: Number of logfiles referencing to this script + as a postrotate script + @type: int + ''' + + self._last_files = 0 + ''' + @ivar: Number of logfiles referencing to this script + as a lastaction script + @type: int + ''' + + self._done_firstrun = False + ''' + @ivar: Flag, whether the script was executed as + a firstaction script + @type: bool + ''' + + self._done_prerun = False + ''' + @ivar: Flag, whether the script was executed as + a prerun script + @type: bool + ''' + + self._done_postrun = False + ''' + @ivar: Flag, whether the script was executed as + a postrun script + @type: bool + ''' + + self._done_lastrun = False + ''' + @ivar: Flag, whether the script was executed as + a lastaction script + @type: bool + ''' + + self._do_post = False + ''' + Runtime flag, that the script should be executed + as an postrun script + ''' + + self._do_last = False + ''' + Runtime flag, that the script should be executed + as an lastaction script + ''' + + #------------------------------------------------------------ + # Defintion of some properties + + #------------------------------------------------------------ + # Property 'name' + def _get_name(self): + ''' + Getter method for property 'name' + ''' + return self._name + + name = property(_get_name, None, None, "Name of the script as an identifier") + + #------------------------------------------------------------ + # Property 'cmd' + def _get_cmd(self): + ''' + Getter method for property 'cmd' + ''' + if len(self._cmd): + return "\n".join(self._cmd) + else: + return None + + def _set_cmd(self, value): + ''' + Setter method for property 'cmd' + ''' + if value: + if isinstance(value, list): + self._cmd = value[:] + else: + self._cmd = [value] + else: + self._cmd = [] + + def _del_cmd(self): + ''' + Deleter method for property 'cmd' + ''' + self._cmd = [] + + cmd = property(_get_cmd, _set_cmd, _del_cmd, "the commands to execute") + + #------------------------------------------------------------ + # Property 'post_files' + def _get_post_files(self): + ''' + Getter method for property 'post_files' + ''' + return self._post_files + + def _set_post_files(self, value): + ''' + Setter method for property 'post_files' + ''' + _ = self.t.lgettext + if isinstance(value, int): + self._post_files = value + else: + msg = _("Invalid value for property '%s' given.") % ('post_files') + raise LogRotateScriptError(msg) + + post_files = property( + _get_post_files, + _set_post_files, + None, + "Number of logfiles referencing to this script as a postrotate script." + ) + + #------------------------------------------------------------ + # Property 'last_files' + def _get_last_files(self): + ''' + Getter method for property 'last_files' + ''' + return self._last_files + + def _set_last_files(self, value): + ''' + Setter method for property 'last_files' + ''' + _ = self.t.lgettext + if isinstance(value, int): + self._last_files = value + else: + msg = _("Invalid value for property '%s' given.") % ('last_files') + raise LogRotateScriptError(msg) + + last_files = property( + _get_last_files, + _set_last_files, + None, + "Number of logfiles referencing to this script as a lastaction script." + ) + + #------------------------------------------------------------ + # Property 'done_firstrun' + def _get_done_firstrun(self): + ''' + Getter method for property 'done_firstrun' + ''' + return self._done_firstrun + + def _set_done_firstrun(self, value): + ''' + Setter method for property 'done_firstrun' + ''' + self._done_firstrun = bool(value) + + done_firstrun = property( + _get_done_firstrun, + _set_done_firstrun, + None, + "Flag, whether the script was executed as a firstaction script." + ) + + #------------------------------------------------------------ + # Property 'done_prerun' + def _get_done_prerun(self): + ''' + Getter method for property 'done_prerun' + ''' + return self._done_prerun + + def _set_done_prerun(self, value): + ''' + Setter method for property 'done_prerun' + ''' + self._done_prerun = bool(value) + + done_prerun = property( + _get_done_prerun, + _set_done_prerun, + None, + "Flag, whether the script was executed as a prerun script." + ) + + #------------------------------------------------------------ + # Property 'done_postrun' + def _get_done_postrun(self): + ''' + Getter method for property 'done_postrun' + ''' + return self._done_postrun + + def _set_done_postrun(self, value): + ''' + Setter method for property 'done_postrun' + ''' + self._done_postrun = bool(value) + + done_postrun = property( + _get_done_postrun, + _set_done_postrun, + None, + "Flag, whether the script was executed as a postrun script." + ) + + #------------------------------------------------------------ + # Property 'done_lastrun' + def _get_done_lastrun(self): + ''' + Getter method for property 'done_lastrun' + ''' + return self._done_lastrun + + def _set_done_lastrun(self, value): + ''' + Setter method for property 'done_lastrun' + ''' + self._done_lastrun = bool(value) + + done_lastrun = property( + _get_done_lastrun, + _set_done_lastrun, + None, + "Flag, whether the script was executed as a lastaction script." + ) + + #------------------------------------------------------------ + # Property 'do_post' + def _get_do_post(self): + ''' + Getter method for property 'do_post' + ''' + return self._do_post + + def _set_do_post(self, value): + ''' + Setter method for property 'do_post' + ''' + self._do_post = bool(value) + + do_post = property( + _get_do_post, + _set_do_post, + None, + "Flag, whether the script should be executed as a postrun script." + ) + + #------------------------------------------------------------ + # Property 'do_last' + def _get_do_last(self): + ''' + Getter method for property 'do_last' + ''' + return self._do_last + + def _set_do_last(self, value): + ''' + Setter method for property 'do_last' + ''' + self._do_last = bool(value) + + do_last = property( + _get_do_last, + _set_do_last, + None, + "Flag, whether the script should be executed as a lastaction script." + ) + + #------------------------------------------------------------ + # Other Methods + + #------------------------------------------------------- + def __del__(self): + ''' + Destructor. + Checks, whether the script should even be run as + a postrun or a lastaction script + ''' + + _ = self.t.lgettext + if self.verbose > 2: + msg = _("Logrotate script object '%s' will destroyed.") % (self.name) + self.logger.debug(msg) + + self.check_for_execute() + + #------------------------------------------------------------ + def __str__(self): + ''' + Typecasting function for translating object structure + into a string + + @return: structure as string + @rtype: str + ''' + + pp = pprint.PrettyPrinter(indent=4) + structure = self.as_dict() + return pp.pformat(structure) + + #------------------------------------------------------- + def as_dict(self): + ''' + Transforms the elements of the object into a dict + + @return: structure as dict + @rtype: dict + ''' + + res = {} + res['t'] = self.t + res['verbose'] = self.verbose + res['name'] = self.name + res['test_mode'] = self.test_mode + res['logger'] = self.logger + res['cmd'] = self._cmd[:] + res['post_files'] = self.post_files + res['last_files'] = self.last_files + res['done_firstrun'] = self.done_firstrun + res['done_prerun'] = self.done_prerun + res['done_postrun'] = self.done_postrun + res['done_lastrun'] = self.done_lastrun + res['do_post'] = self.do_post + res['do_last'] = self.do_last + + return res + + #------------------------------------------------------------ + def add_cmd(self, cmd): + ''' + Adding a command to the list self._cmd + + @param cmd: the command to add to self._cmd + @type cmd: str + + @return: None + ''' + self._cmd.append(cmd) + + #------------------------------------------------------------ + def execute(self, force=False, expected_retcode=0): + ''' + Executes the command as an OS command in a shell. + + @param force: force executing command even + if self.test_mode == True + @type force: bool + @param expected_retcode: expected returncode of the command + (should be 0) + @type expected_retcode: int + + @return: Success of the comand (shell returncode == 0) + @rtype: bool + ''' + + _ = self.t.lgettext + cmd = self.cmd + if cmd is None: + msg = _("No command to execute defined in script '%s'.") % (self.name) + raise LogRotateScriptError(msg) + return False + if self.verbose > 3: + msg = _("Executing script '%(name)s' with command: '%(cmd)s'") \ + % {'name': self.name, 'cmd': cmd} + self.logger.debug(msg) + if not force: + if self.test_mode: + return True + try: + retcode = subprocess.call(command, shell=True) + if self.verbose > 3: + msg = _("Got returncode for script '%(name)s': '%(retcode)s'") \ + % {'name': self.name, 'retcode': retcode} + self.logger.debug(msg) + if retcode < 0: + msg = _("Child in script '%(name)s' was terminated by signal %(retcode)d") \ + % {'name': self.name, 'retcode': -retcode} + self.logger.error(msg) + return False + if retcode != expected_retcode: + return False + return True + except OSError, e: + msg = _("Execution of script '%(name)s' failed: %(error)s") \ + % {'name': self.name, 'error': str(e)} + self.logger.error(msg) + return False + + return False + + #------------------------------------------------------------ + def check_for_execute(self, force=False, expected_retcode=0): + ''' + Checks, whether the script should executed. + + @param force: force executing command even + if self.test_mode == True + @type force: bool + @param expected_retcode: expected returncode of the command + (should be 0) + @type expected_retcode: int + + @return: Success of execution + @rtype: bool + ''' + + _ = self.t.lgettext + msg = _("Checking, whether the script '%s' should be executed.") % (self.name) + self.logger.debug(msg) + + if self.do_post or self.do_last: + result = self.execute(force=force, expected_retcode=expected_retcode) + self.do_post = False + self.do_last = False + return result + + return True + +#======================================================================== + +if __name__ == "__main__": + pass + + +#======================================================================== + +# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotate/LogRotateStatusFile.py b/LogRotate/LogRotateStatusFile.py new file mode 100755 index 0000000..5dc4b3c --- /dev/null +++ b/LogRotate/LogRotateStatusFile.py @@ -0,0 +1,575 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# $Id$ +# $URL$ + +''' +@author: Frank Brehm +@contact: frank@brehm-online.com +@license: GPL3 +@copyright: (c) 2010-2011 by Frank Brehm, Berlin +@version: 0.0.2 +@summary: module for operations with the logrotate state file +''' + +import re +import sys +import os +import os.path +import gettext +import logging +import pprint + +from datetime import tzinfo, timedelta, datetime, date, time + +from LogRotateCommon import split_parts + +revision = '$Revision$' +revision = re.sub( r'\$', '', revision ) +revision = re.sub( r'Revision: ', r'r', revision ) +revision = re.sub( r'\s*$', '', revision ) + +__author__ = 'Frank Brehm' +__copyright__ = '(C) 2011 by Frank Brehm, Berlin' +__contact__ = 'frank@brehm-online.com' +__version__ = '0.1.0 ' + revision +__license__ = 'GPL3' + +#======================================================================== + +class LogrotateStatusFileError(Exception): + ''' + Base class for exceptions in this module. + ''' + +#======================================================================== + +ZERO = timedelta(0) + +class UTC(tzinfo): + """UTC""" + + def utcoffset(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return ZERO + +utc = UTC() + + +#======================================================================== + +class LogrotateStatusFile(object): + ''' + Class for operations with the logrotate state file + + @author: Frank Brehm + @contact: frank@brehm-online.com + ''' + + #------------------------------------------------------- + def __init__( self, file_name, + local_dir = None, + verbose = 0, + test_mode = False, + ): + ''' + Constructor. + + @param file_name: the file name of the status file + @type file_name: str + @param verbose: verbosity (debug) level + @type verbose: int + @param test_mode: test mode - no write actions are made + @type test_mode: bool + @param local_dir: The directory, where the i18n-files (*.mo) + are located. If None, then system default + (/usr/share/locale) is used. + @type local_dir: str or None + + @return: None + ''' + + self.local_dir = local_dir + ''' + @ivar: The directory, where the i18n-files (*.mo) are located. + @type: str or None + ''' + + self.t = gettext.translation( + 'LogRotateStatusFile', + local_dir, + fallback = True + ) + ''' + @ivar: a gettext translation object + @type: gettext.translation + ''' + + _ = self.t.lgettext + + self.verbose = verbose + ''' + @ivar: verbosity level (0 - 9) + @type: int + ''' + + self.file_name = file_name + ''' + @ivar: the initial file name of the status file to use + @type: str + ''' + + self.file_name_is_absolute = False + ''' + @ivar: flag, that shows, that the file name is now an absolute path + @type: bool + ''' + + self.fd = None + ''' + @ivar: the file object of the opened status file, or None, if not opened + @type: file or None + ''' + + self.was_read = False + ''' + @ivar: flag, whether the status file was read + @type: bool + ''' + + self.status_version = None + ''' + @ivar: the version of the status file (2 or 3) + @type: int or None + ''' + + self.test_mode = test_mode + ''' + @ivar: test mode - no write actions are made + @type: bool + ''' + + self.has_changed = False + ''' + @ivar: flag, whether something has changed and needs to be written + @type: bool + ''' + + self.logger = logging.getLogger('pylogrotate.status_file') + ''' + @ivar: logger object + @type: logging.getLogger + ''' + + self.file_state = {} + ''' + @ivar: the last rotation date of every particular log file + keys are the asolute filenames (without globbing) + and the values are datetime objects of the last rotation + referencing to UTC + If no rotation was made, value is datetime.min(). + @type: dict + ''' + + # Initial read and check for permissions + self.read(must_exists = False) + self._check_permissions() + + #------------------------------------------------------- + def __del__(self): + ''' + Destructor. + Enforce saving of status file, if something has changed. + ''' + + _ = self.t.lgettext + msg = _("Status file object will destroyed.") + self.logger.debug(msg) + + if self.has_changed: + self.write() + + #------------------------------------------------------- + def as_dict(self): + ''' + Transforms the elements of the object into a dict + + @return: structure as dict + @rtype: dict + ''' + + res = {} + res['local_dir'] = self.local_dir + res['t'] = self.t + res['verbose'] = self.verbose + res['file_name'] = self.file_name + res['file_name_is_absolute'] = self.file_name_is_absolute + res['fd'] = self.fd + res['status_version'] = self.status_version + res['test_mode'] = self.test_mode + res['logger'] = self.logger + res['file_state'] = self.file_state + res['was_read'] = self.was_read + res['has_changed'] = self.has_changed + + return res + + #------------------------------------------------------------ + def get_rotation_date(self, logfile): + ''' + Gives back the date of the last rotation of a particular logfile. + If this logfile is not found in the state file, datetime.min() is given back. + + @param logfile: the logfile to query + @type logfile: str + + @return: date of last rotation of this logfile + @rtype: datetime + ''' + + if not self.was_read: + self.read(must_exists = False) + + rotate_date = datetime.min.replace(tzinfo=utc) + if logfile in self.file_state: + rotate_date = self.file_state[logfile] + + return rotate_date + + #------------------------------------------------------------ + def set_rotation_date(self, logfile, rotate_date = None): + ''' + Sets the rotation date of the given logfile. + If the rotation date is not given, datetime.utcnow() is used. + + @param logfile: the logfile to set + @type logfile: str + @param rotate_date: the rotation date of this logfile + @type rotate_date: datetime or None + + @return: date of rotation of this logfile (relative to UTC) + @rtype: datetime + ''' + + date_utc = datetime.utcnow() + if rotate_date: + date_utc = rotate_date.astimezone(utc) + + _ = self.t.lgettext + msg = _("Setting rotation date of '%(file)s' to '%(date)s' ...") \ + % {'file': logfile, 'date': date_utc.isoformat(' ') } + self.logger.debug(msg) + + #self.read(must_exists = False) + self.file_state[logfile] = date_utc + self.has_changed = True + + #self.write() + + return date_utc + + #------------------------------------------------------------ + def write(self): + ''' + Writes the content of self.file_state in the state file. + + @return: success of writing + @rtype: bool + ''' + + _ = self.t.lgettext + + # setting a failing version of the status file + if not self.status_version: + self.status_version = 3 + + max_length = 1 + + # Retrieving the maximum length of the logfiles for version 3 + if self.status_version == 3: + for logfile in self.file_state: + if len(logfile) > max_length: + max_length = len(logfile) + max_length += 2 + + fd = None + # Big try block for ensure closing open status file + try: + + msg = _("Open status file '%s' for writing ...") % (self.file_name) + self.logger.debug(msg) + + # open status file for writing + if not self.test_mode: + try: + fd = open(self.file_name, 'w') + except IOError, e: + msg = _("Could not open status file '%s' for write: ") % (self.file_name) + str(e) + raise LogrotateStatusFileError(msg) + + # write logrotate version line + line = 'Logrotate State -- Version 3' + if self.status_version == 2: + line = 'logrotate state -- version 2' + if self.verbose > 2: + msg = _("Writing version line '%s'.") % (line) + self.logger.debug(msg) + line += '\n' + if fd: + fd.write(line) + + # iterate over logfiles in self.file_state + for logfile in sorted(self.file_state.keys(), lambda x,y: cmp(x.lower(), y.lower())): + rotate_date = self.file_state[logfile] + date_str = "%d-%d-%d" % (rotate_date.year, rotate_date.month, rotate_date.day) + if self.status_version == 3: + date_str = ( "%d-%02d-%02d_%02d:%02d:%02d" % + (rotate_date.year, rotate_date.month, rotate_date.day, + rotate_date.hour, rotate_date.minute, rotate_date.second)) + line = '%-*s %s' % (max_length, ('"' + logfile + '"'), date_str) + if self.verbose > 2: + msg = _("Writing line '%s'.") % (line) + self.logger.debug(msg) + if fd: + fd.write(line + "\n") + + finally: + if fd: + fd.close() + fd = None + + self.has_changed = False + return True + + #------------------------------------------------------------ + def __str__(self): + ''' + Typecasting function for translating object structure + into a string + + @return: structure as string + @rtype: str + ''' + + pp = pprint.PrettyPrinter(indent=4) + return pp.pformat(self.as_dict()) + + #------------------------------------------------------------ + def _check_permissions(self): + ''' + Checks the permissions of the state file and/or his parent directory. + Throws a LogrotateStatusFileError on a error. + + @return: success of check + @rtype: bool + ''' + + _ = self.t.lgettext + msg = _("Checking permissions of status file '%s' ...") % (self.file_name) + self.logger.debug(msg) + + if os.path.exists(self.file_name): + # Check for write access to the status file + if os.access(self.file_name, os.W_OK): + msg = _("Access to status file '%s' is OK.") % (self.file_name) + self.logger.debug(msg) + return True + else: + msg = _("No write access to status file '%s'.") % (self.file_name) + if self.test_mode: + self.logger.error(msg) + else: + raise LogrotateStatusFileError(msg) + return False + + parent_dir = os.path.dirname(self.file_name) + msg = _("Checking permissions of parent directory '%s' ...") % (parent_dir) + self.logger.debug(msg) + + # Check for existence of parent dir + if not os.path.exists(parent_dir): + msg = _("Directory '%s' doesn't exists.") % (parent_dir) + if self.test_mode: + self.logger.error(msg) + else: + raise LogrotateStatusFileError(msg) + return False + + # Check whether parent dir is a directory + if not os.path.isdir(parent_dir): + msg = _("Parent directory '%(dir)s' of status file '%(file)s' is not a directory.") \ + % {'dir': parent_dir, 'file': self.file_name } + if self.test_mode: + self.logger.error(msg) + else: + raise LogrotateStatusFileError(msg) + return False + + # Check for write access to parent dir + if not os.access(parent_dir, os.W_OK): + msg = _("No write access to parent directory '%(dir)s' of status file '%(file)s'.") \ + % {'dir': parent_dir, 'file': self.file_name } + if self.test_mode: + self.logger.error(msg) + else: + raise LogrotateStatusFileError(msg) + return False + + msg = _("Permissions to parent directory '%s' are OK.") % (parent_dir) + self.logger.debug(msg) + return True + + #------------------------------------------------------- + def read(self, must_exists = True): + ''' + Reads the status file and put the results in the dict self.file_state. + Puts back the absolute path of the status file in self.file_name on success. + + Throws a LogrotateStatusFileError on a error. + + @param must_exists: throws an exception, if true and the status file + doesn't exists + @type must_exists: bool + + @return: success of reading + @rtype: bool + ''' + + self.file_state = {} + _ = self.t.lgettext + + # Check for existence of status file + if not os.path.exists(self.file_name): + msg = _("Status file '%s' doesn't exists.") % (self.file_name) + if must_exists: + raise LogrotateStatusFileError(msg) + else: + self.logger.info(msg) + return False + + # makes the name of the status file an absolute path + if not self.file_name_is_absolute: + self.file_name = os.path.abspath(self.file_name) + self.file_name_is_absolute = True + if self.verbose > 2: + msg = _("Absolute path of status file is now '%s'.") % (self.file_name) + self.logger.debug(msg) + + # Checks, that the status file is a regular file + if not os.path.isfile(self.file_name): + msg = _("Status file '%s' is not a regular file.") % (self.file_name) + raise LogrotateStatusFileError(msg) + return False + + msg = _("Reading status file '%s' ...") % (self.file_name) + self.logger.debug(msg) + + fd = None + try: + fd = open(self.file_name, 'Ur') + except IOError, e: + msg = _("Could not read status file '%s': ") % (self.file_name) + str(e) + raise LogrotateStatusFileError(msg) + self.fd = fd + + try: + # Reading the lines of the status file + i = 0 + for line in fd: + i += 1 + line = line.strip() + if self.verbose > 4: + msg = _("Performing status file line '%(line)s' (file: '%(file)s', row: %(row)d)") \ + % {'line': line, 'file': self.file_name, 'row': i, } + self.logger.debug(msg) + + # check for file heading + if i == 1: + match = re.search(r'^logrotate\s+state\s+-+\s+version\s+([23])$', line, re.IGNORECASE) + if match: + # Correct file header + self.status_version = int(match.group(1)) + if self.verbose > 1: + msg = _("Idendified version of status file: %d") % (self.status_version) + self.logger.debug(msg) + continue + else: + # Wrong header + msg = _("Incompatible version of status file '%(file)s': %(header)s") \ + % { 'file': self.file_name, 'header': line } + fd.close() + raise LogrotateStatusFileError(msg) + + if line == '': + continue + + parts = split_parts(line) + logfile = parts[0] + rdate = parts[1] + if self.verbose > 2: + msg = _("Found logfile '%(file)s' with rotation date '%(date)s'.") \ + % { 'file': logfile, 'date': rdate } + self.logger.debug(msg) + + if logfile and rdate: + match = re.search(r'\s*(\d+)[_\-](\d+)[_\-](\d+)(?:[\s\-_]+(\d+)[_\-:](\d+)[_\-:](\d+))?', rdate) + if not match: + msg = _("Could not determine date format: '%(date)s' (file: '%(file)s', row: %(row)d)") \ + % {'date': rdate, 'file': logfile, 'row': i, } + self.logger.warning(msg) + continue + d = { + 'Y': int(match.group(1)), + 'm': int(match.group(2)), + 'd': int(match.group(3)), + 'H': 0, + 'M': 0, + 'S': 0, + } + if match.group(4) is not None: + d['H'] = int(match.group(4)) + if match.group(5) is not None: + d['M'] = int(match.group(5)) + if match.group(6) is not None: + d['S'] = int(match.group(6)) + + dt = None + try: + dt = datetime(d['Y'], d['m'], d['d'], d['H'], d['M'], d['S'], tzinfo = utc) + except ValueError, e: + msg = _("Invalid date: '%(date)s' (file: '%(file)s', row: %(row)d)") \ + % {'date': rdate, 'file': logfile, 'row': i, } + self.logger.warning(msg) + continue + + self.file_state[logfile] = dt + + else: + + msg = _("Neither a logfile nor a date found in line '%(line)s' (file: '%(file)s', row: %(row)d)") \ + % {'line': line, 'file': logfile, 'row': i, } + self.logger.warning(msg) + + finally: + fd.close + + self.fd = None + self.was_read = True + + return True + +#======================================================================== + +if __name__ == "__main__": + pass + + +#======================================================================== + +# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotate/__init__.py b/LogRotate/__init__.py new file mode 100755 index 0000000..2c6a1ef --- /dev/null +++ b/LogRotate/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +''' +@author: Frank Brehm +@contact: frank@brehm-online.com +@copyright: (c) 2010 - 2011 by Frank Brehm, Berlin +@summary: All modules for logrotate.py +''' +# vim: fileencoding=utf-8 filetype=python ts=4 diff --git a/LogRotateCommon.py b/LogRotateCommon.py deleted file mode 100755 index af03d1b..0000000 --- a/LogRotateCommon.py +++ /dev/null @@ -1,512 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# $Id$ -# $URL$ - -''' -@author: Frank Brehm -@contact: frank@brehm-online.com -@license: GPL3 -@copyright: (c) 2010-2011 by Frank Brehm, Berlin -@version: 0.1.0 -@summary: Module for common used functions -''' - -import re -import sys -import locale -import logging -import gettext -import csv -import pprint -import email.utils - -revision = '$Revision$' -revision = re.sub( r'\$', '', revision ) -revision = re.sub( r'Revision: ', r'r', revision ) -revision = re.sub( r'\s*$', '', revision ) - -__author__ = 'Frank Brehm' -__copyright__ = '(C) 2011 by Frank Brehm, Berlin' -__contact__ = 'frank@brehm-online.com' -__version__ = '0.1.0 ' + revision -__license__ = 'GPL3' - - -logger = logging.getLogger('pylogrotate.common') -locale_dir = None - -#======================================================================== - -def split_parts( text, keep_quotes = False, raise_on_unbalanced = True): - ''' - Split the given text in chunks by whitespaces or - single or double quoted strings. - - @param text: the text to split in chunks - @type text: str - @param keep_quotes: keep quotes of quoted chunks - @type keep_quotes: bool - @param raise_on_unbalanced: raise an exception on - unbalanced quotes - @type raise_on_unbalanced: bool - - @return: list of chunks - @rtype: list - ''' - - chunks = [] - if text is None: - return chunks - - txt = str(text) - last_chunk = '' - - # Big loop to split the text - until it is empty - while txt != '': - - # add chunk, if there is a chunk left and a whitspace - # at the begin of the line - match = re.search(r"\s+", txt) - if ( last_chunk != '' ) and match: - chunks.append(last_chunk) - last_chunk = '' - - # clean the line - txt = txt.strip() - if txt == '': - break - - # search for a single quoted string at the begin of the line - match = re.search(r"^'((?:\\'|[^'])*)'", txt) - if match: - chunk = match.group(1) - chunk = re.sub(r"\\'", "'", chunk) - if keep_quotes: - chunk = "'" + chunk + "'" - last_chunk += chunk - txt = re.sub(r"^'(?:\\'|[^'])*'", "", txt) - continue - - # search for a double quoted string at the begin of the line - match = re.search(r'^"((?:\\"|[^"])*)"', txt) - if match: - chunk = match.group(1) - chunk = re.sub(r'\\"', '"', chunk) - if keep_quotes: - chunk = '"' + chunk + '"' - last_chunk += chunk - txt = re.sub(r'^"(?:\\"|[^"])*"', "", txt) - continue - - # search for unquoted, whitespace delimited text - # at the begin of the line - match = re.search(r'^((?:[^\s\'"]+|\\\'|\\")+)', txt) - if match: - last_chunk += match.group(1) - txt = re.sub(r'^(?:[^\s\'"]+|\\\'|\\")+', "", txt) - continue - - # Only whitespaces left - match = re.search(r'^\s*$', txt) - if match: - break - - # Check for unbalanced quotes - match = re.search(r'^([\'"].*)\s*', txt) - if match: - chunk = match.group(1) - if raise_on_unbalanced: - raise Exception("Unbalanced quotes in »%s«." % ( str(text) ) ) - else: - last_chunk += chunk - continue - - # Here we should not come to ... - raise Exception("Broken split of »%s«: »%s« left" %( str(text), txt)) - - if last_chunk != '': - chunks.append(last_chunk) - - return chunks - -#------------------------------------------------------------------------ - -def email_valid(address): - ''' - Simple Check for E-Mail addresses - - @param address: the mail address to check - @type address: str - - @return: Validity of the given mil address - @rtype: bool - ''' - - if address is None: - return False - - adr = str(address) - if adr is None or adr == '': - return False - - pattern = r'^[a-z0-9._%-+]+@[a-z0-9._%-]+.[a-z]{2,6}$' - if re.search(pattern, adr, re.IGNORECASE) is None: - return False - - return True - -#------------------------------------------------------------------------ - -def human2bytes(value, si_conform = True, use_locale_radix = False, verbose = 0): - ''' - Converts the given human readable byte value (e.g. 5MB, 8.4GiB etc.) - with a prefix into an integer/long value (without a prefix). - It raises a ValueError on invalid values. - - Available prefixes are: - - kB (1000), KB (1024), KiB (1024) - - MB (1000*1000), MiB (1024*1024) - - GB (1000³), GiB (1024³) - - TB (1000^4), TiB (1024^4) - - PB (1000^5), PiB (1024^5) - - @param value: the value to convert - @type value: str - @param si_conform: use factor 1000 instead of 1024 for kB a.s.o. - @type si_conform: bool - @param use_locale_radix: use the locale version of radix instead of the - english decimal dot. - @type use_locale_radix: bool - @param verbose: level of verbosity - @type verbose: int - - @return: amount of bytes - @rtype: long - ''' - - t = gettext.translation('LogRotateCommon', locale_dir, fallback=True) - _ = t.lgettext - - if value is None: - msg = _("Given value is 'None'.") - raise ValueError(msg) - - radix = '.' - if use_locale_radix: - radix = locale.RADIXCHAR - radix = re.escape(radix) - if verbose > 5: - msg = _("using radix '%s'.") % (radix) - logger.debug(msg) - - value_raw = '' - prefix = None - pattern = r'^\s*\+?(\d+(?:' + radix + r'\d*)?)\s*(\S+)?' - match = re.search(pattern, value) - if match is not None: - value_raw = match.group(1) - prefix = match.group(2) - else: - msg = _("Could not determine bytes in '%s'.") % (value) - raise ValueError(msg) - - if use_locale_radix: - value_raw = re.sub(radix, '.', value_raw, 1) - value_float = float(value_raw) - if prefix is None: - prefix = '' - - factor_bin = long(1024) - factor_si = long(1000) - if not si_conform: - factor_si = factor_bin - - factor = long(1) - - if re.search(r'^\s*(?:b(?:yte)?)?\s*$', prefix, re.IGNORECASE): - factor = long(1) - elif re.search(r'^\s*k(?:[bB](?:[Yy][Tt][Ee])?)?\s*$', prefix): - factor = factor_si - elif re.search(r'^\s*Ki?(?:[bB](?:[Yy][Tt][Ee])?)?\s*$', prefix): - factor = factor_bin - elif re.search(r'^\s*M(?:B(?:yte)?)?\s*$', prefix, re.IGNORECASE): - factor = (factor_si * factor_si) - elif re.search(r'^\s*MiB(?:yte)?\s*$', prefix, re.IGNORECASE): - factor = (factor_bin * factor_bin) - elif re.search(r'^\s*G(?:B(?:yte)?)?\s*$', prefix, re.IGNORECASE): - factor = (factor_si * factor_si * factor_si) - elif re.search(r'^\s*GiB(?:yte)?\s*$', prefix, re.IGNORECASE): - factor = (factor_bin * factor_bin * factor_bin) - elif re.search(r'^\s*T(?:B(?:yte)?)?\s*$', prefix, re.IGNORECASE): - factor = (factor_si * factor_si * factor_si * factor_si) - elif re.search(r'^\s*TiB(?:yte)?\s*$', prefix, re.IGNORECASE): - factor = (factor_bin * factor_bin * factor_bin * factor_bin) - elif re.search(r'^\s*P(?:B(?:yte)?)?\s*$', prefix, re.IGNORECASE): - factor = (factor_si * factor_si * factor_si * factor_si * factor_si) - elif re.search(r'^\s*PiB(?:yte)?\s*$', prefix, re.IGNORECASE): - factor = (factor_bin * factor_bin * factor_bin * factor_bin * factor_bin) - else: - msg = _("Couldn't detect prefix '%s'.") % (prefix) - raise ValueError(msg) - - if verbose > 5: - msg = _("Found factor %d.") % (factor) - logger.debug(msg) - - return long(factor * value_float) - -#------------------------------------------------------------------------ - -def period2days(period, use_locale_radix = False, verbose = 0): - ''' - Converts the given string of the form »5d 8h« in an amount of days. - It raises a ValueError on invalid values. - - Special values of period: - - now (returns 0) - - never (returns float('inf')) - - Valid units for periods are: - - »h[ours]« - - »d[ays]« - default, if bare numbers are given - - »w[eeks]« - == 7 days - - »m[onths]« - == 30 days - - »y[ears]« - == 365 days - - @param period: the period to convert - @type period: str - @param use_locale_radix: use the locale version of radix instead of the - english decimal dot. - @type use_locale_radix: bool - @param verbose: level of verbosity - @type verbose: int - - @return: amount of days - @rtype: float - ''' - - t = gettext.translation('LogRotateCommon', locale_dir, fallback=True) - _ = t.lgettext - - if period is None: - msg = _("Given period is 'None'.") - raise ValueError(msg) - - value = str(period).strip().lower() - if period == '': - msg = _("Given period was empty") - raise ValueError(msg) - - if verbose > 4: - msg = _("Called with '%s'.") % (period) - logger.debug(msg) - - if period == 'now': - return float(0) - - # never - returns a positive infinite value - if period == 'never': - return float('inf') - - days = float(0) - radix = '.' - if use_locale_radix: - radix = locale.RADIXCHAR - radix = re.escape(radix) - if verbose > 5: - msg = _("Using radix '%s'.") % (radix) - logger.debug(msg) - - # Search for hours in value - pattern = r'(\d+(?:' + radix + r'\d*)?)\s*h(?:ours?)?' - if verbose > 5: - msg = _("Pattern '%s'.") % (pattern) - logger.debug(msg) - match = re.search(pattern, value, re.IGNORECASE) - if match: - hours_str = match.group(1) - if use_locale_radix: - hours_str = re.sub(radix, '.', hours_str, 1) - hours = float(hours_str) - days += (hours/24) - if verbose > 4: - msg = _("Found %f hours.") % (hours) - logger.debug(msg) - value = re.sub(pattern, '', value, re.IGNORECASE) - if verbose > 5: - msg = _("Rest after hours: '%s'." % (value)) - logger.debug(msg) - - # Search for weeks in value - pattern = r'(\d+(?:' + radix + r'\d*)?)\s*w(?:eeks?)?' - if verbose > 5: - msg = _("Pattern '%s'.") % (pattern) - logger.debug(msg) - match = re.search(pattern, value, re.IGNORECASE) - if match: - weeks_str = match.group(1) - if use_locale_radix: - weeks_str = re.sub(radix, '.', weeks_str, 1) - weeks = float(weeks_str) - days += (weeks*7) - if verbose > 4: - msg = _("Found %f weeks.") % (weeks) - logger.debug(msg) - value = re.sub(pattern, '', value, re.IGNORECASE) - if verbose > 5: - msg = _("Rest after weeks: '%s'." % (value)) - logger.debug(msg) - - # Search for months in value - pattern = r'(\d+(?:' + radix + r'\d*)?)\s*m(?:onths?)?' - if verbose > 5: - msg = _("Pattern '%s'.") % (pattern) - logger.debug(msg) - match = re.search(pattern, value, re.IGNORECASE) - if match: - months_str = match.group(1) - if use_locale_radix: - months_str = re.sub(radix, '.', months_str, 1) - months = float(months_str) - days += (months*30) - if verbose > 4: - msg = _("Found %f months.") % (months) - logger.debug(msg) - value = re.sub(pattern, '', value, re.IGNORECASE) - if verbose > 5: - msg = _("Rest after months: '%s'." % (value)) - logger.debug(msg) - - # Search for years in value - pattern = r'(\d+(?:' + radix + r'\d*)?)\s*y(?:ears?)?' - if verbose > 5: - msg = _("Pattern '%s'.") % (pattern) - logger.debug(msg) - match = re.search(pattern, value, re.IGNORECASE) - if match: - years_str = match.group(1) - if use_locale_radix: - years_str = re.sub(radix, '.', years_str, 1) - years = float(years_str) - days += (years*365) - if verbose > 4: - msg = _("Found %f years.") % (years) - logger.debug(msg) - value = re.sub(pattern, '', value, re.IGNORECASE) - if verbose > 5: - msg = _("Rest after years: '%s'." % (value)) - logger.debug(msg) - - # At last search for days in value - pattern = r'(\d+(?:' + radix + r'\d*)?)\s*(?:d(?:ays?)?)?' - if verbose > 5: - msg = _("Pattern '%s'.") % (pattern) - logger.debug(msg) - match = re.search(pattern, value, re.IGNORECASE) - if match: - days_str = match.group(1) - if use_locale_radix: - days_str = re.sub(radix, '.', days_str, 1) - days_float = float(days_str) - days += days_float - if verbose > 4: - msg = _("Found %f days.") % (days_float) - logger.debug(msg) - value = re.sub(pattern, '', value, re.IGNORECASE) - if verbose > 5: - msg = _("Rest after days: '%s'." % (value)) - logger.debug(msg) - - # warn, if there is a rest - if re.search(r'^\s*$', value) is None: - msg = _("Invalid content for a period: '%s'.") % (value) - logger.warning(msg) - - if verbose > 4: - msg = _("Total %f days found.") % (days) - logger.debug(msg) - - return days - -#------------------------------------------------------------------------ - -def get_address_list(address_str, verbose = 0): - ''' - Retrieves all mail addresses from address_str and give them back - as a list of tuples. - - @param address_str: the string with all mail addresses as a comma - separated list - @type address_str: str - @param verbose: level of verbosity - @type verbose: int - - @return: list of tuples in the form of the return value - of email.utils.parseaddr() - @rtype: list - - ''' - - t = gettext.translation('LogRotateCommon', locale_dir, fallback=True) - _ = t.lgettext - pp = pprint.PrettyPrinter(indent=4) - - addr_list = [] - addresses = [] - - for row in csv.reader([address_str], doublequote=False, skipinitialspace=True): - for address in row: - addr_list.append(address) - - if verbose > 2: - msg = _("Found address entries:") + "\n" + pp.pformat(addr_list) - logger.debug(msg) - - for address in addr_list: - address = re.sub(r',', ' ', address) - address = re.sub(r'\s+', ' ', address) - pair = email.utils.parseaddr(address) - if verbose > 2: - msg = _("Got mail address pair:") + "\n" + pp.pformat(pair) - logger.debug(msg) - if not email_valid(pair[1]): - msg = _("Found invalid mail address '%s'.") % (address) - logger.warning(msg) - continue - addresses.append(pair) - - return addresses - -#------------------------------------------------------------------------ - -def to_unicode_or_bust(obj, encoding='utf-8'): - ''' - Transforms a string, what is not a unicode string, into a unicode string. - All other objects are left untouched. - - @param obj: the object to transform - @type obj: object - @param encoding: the encoding to use to decode the object - defaults to 'utf-8' - @type encoding: str - - @return: the maybe decoded object - @rtype: object - ''' - - if isinstance(obj, basestring): - if not isinstance(obj, unicode): - obj = unicode(obj, encoding) - - return obj - -#======================================================================== - -if __name__ == "__main__": - pass - -#======================================================================== - -# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotateConfig.py b/LogRotateConfig.py deleted file mode 100755 index ba613de..0000000 --- a/LogRotateConfig.py +++ /dev/null @@ -1,2038 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# $Id$ -# $URL$ - -''' -@author: Frank Brehm -@contact: frank@brehm-online.com -@license: GPL3 -@copyright: (c) 2010-2011 by Frank Brehm, Berlin -@version: 0.0.2 -@summary: module the configuration parsing object for Python logrotating -''' - -import re -import sys -import gettext -import pprint -import os -import os.path -import pwd -import grp -import glob -import logging -import email.utils - -from LogRotateCommon import split_parts, email_valid, period2days, human2bytes -from LogRotateCommon import get_address_list -from LogRotateScript import LogRotateScript - -revision = '$Revision$' -revision = re.sub( r'\$', '', revision ) -revision = re.sub( r'Revision: ', r'r', revision ) -revision = re.sub( r'\s*$', '', revision ) - -__author__ = 'Frank Brehm' -__copyright__ = '(C) 2011 by Frank Brehm, Berlin' -__contact__ = 'frank@brehm-online.com' -__version__ = '0.1.2 ' + revision -__license__ = 'GPL3' - - -#======================================================================== -# Module variables - -# @var: dict with all valid taboo pattern types as keys -# and the resulting regex template for the filename as value -pattern_types = { - 'ext': r'%s$', - 'file': r'^%s$', - 'prefix': r'^%s', -} - -script_directives = [ - 'postrotate', - 'prerotate', - 'firstaction', - 'lastaction', -] - -unsupported_options = ( - 'uncompresscmd', - 'error', -) - -options_with_values = ( - 'mail', - 'compresscmd', - 'statusfile', - 'pidfile', - 'compressext', - 'rotate', - 'maxage', - 'mailfrom', - 'smtphost', - 'smtpport', - 'smtptls', - 'smtpuser', - 'smtppasswd', -) - -boolean_options = ( - 'compress', - 'copy', - 'copytruncate', - 'ifempty', - 'missingok', - 'sharedscripts', -) - -integer_options = ( - 'delaycompress', - 'rotate', - 'start', -) - -string_options = ( - 'extension', - 'compresscmd', - 'compressext', - 'compressoptions', -) - -global_options = ( - 'statusfile', - 'pidfile', - 'mailfrom', - 'smtphost', - 'smtpport', - 'smtptls', - 'smtpuser', - 'smtppasswd', -) - -path_options = ( - 'statusfile', - 'pidfile', -) - -valid_periods = { - 'hourly': (1/24), - '2hourly': (1/12), - '4hourly': (1/6), - '6hourly': (1/4), - '12hourly': (1/2), - 'daily': 1, - '2daily': 2, - 'weekly': 7, - 'monthly': 30, - '2monthly': 60, - '4monthly': 120, - '6monthly': 182, - 'yearly': 365, -} - -yes_values = ( - '1', - 'on', - 'y', - 'yes', - 'true', -) - -no_values = ( - '0', - 'off', - 'n', - 'no', - 'false', -) - - -#======================================================================== - -class LogrotateConfigurationError(Exception): - ''' - Base class for exceptions in this module. - ''' - -#======================================================================== - -class LogrotateConfigurationReader(object): - ''' - Class for reading the configuration for Python logrotating - - @author: Frank Brehm - @contact: frank@brehm-online.com - ''' - - #------------------------------------------------------- - def __init__( self, config_file, - verbose = 0, - local_dir = None, - test_mode = False, - ): - ''' - Constructor. - - @param config_file: the configuration file to use - @type config_file: str - @param verbose: verbosity (debug) level - @type verbose: int - @param local_dir: The directory, where the i18n-files (*.mo) - are located. If None, then system default - (/usr/share/locale) is used. - @type local_dir: str or None - @param test_mode: test mode - no write actions are made - @type test_mode: bool - - @return: None - ''' - - self.local_dir = local_dir - ''' - @ivar: The directory, where the i18n-files (*.mo) are located. - @type: str or None - ''' - - self.t = gettext.translation( - 'LogRotateConfig', - local_dir, - fallback = True - ) - ''' - @ivar: a gettext translation object - @type: gettext.translation - ''' - - _ = self.t.lgettext - - self.verbose = verbose - ''' - @ivar: verbosity level (0 - 9) - @type: int - ''' - - self.config_file = config_file - ''' - @ivar: the initial configuration file to use - @type: str - ''' - - self.test_mode = test_mode - ''' - @ivar: test mode - no write actions are made - @type: bool - ''' - - self.logger = logging.getLogger('pylogrotate.config') - ''' - @ivar: logger object - @type: logging.getLogger - ''' - - self.global_option = {} - ''' - @ivar: all global options - @type: dict - ''' - self.global_option['smtphost'] = 'localhost' - - ############################################# - # the rest of instance variables: - - self.search_path = ['/bin', '/usr/bin'] - ''' - @ivar: ordered list with directories, where executables are searched - @type: list - ''' - self._init_search_path() - - self.shred_command = '/usr/bin/shred' - ''' - @ivar: the system command to shred aged rotated logfiles, if wanted - @type: str - ''' - self.check_shred_command() - - self.default = {} - ''' - @ivar: the default values for directives - @type: dict - ''' - self._reset_defaults() - - self.new_log = None - ''' - @ivar: struct with the current log definition - @type: dict or None - ''' - - self.taboo = [] - ''' - @ivar: taboo patterns for including files of whole directories - @type: list - ''' - self.add_taboo(r'\.rpmnew', 'ext'); - self.add_taboo(r'\.rpmorig', 'ext'); - self.add_taboo(r'\.rpmsave', 'ext'); - self.add_taboo(r',v', 'ext'); - self.add_taboo(r'\.swp', 'ext'); - self.add_taboo(r'~', 'ext'); - self.add_taboo(r'\.', 'prefix'); - self.add_taboo(r'\.bak', 'ext'); - self.add_taboo(r'\.old', 'ext'); - self.add_taboo(r'\.rej', 'ext'); - self.add_taboo(r'CVS', 'file'); - self.add_taboo(r'RCS', 'file'); - self.add_taboo(r'\.disabled', 'ext'); - self.add_taboo(r'\.dpkg-old', 'ext'); - self.add_taboo(r'\.dpkg-dist', 'ext'); - self.add_taboo(r'\.dpkg-new', 'ext'); - self.add_taboo(r'\.cfsaved', 'ext'); - self.add_taboo(r'\.ucf-old', 'ext'); - self.add_taboo(r'\.ucf-dist', 'ext'); - self.add_taboo(r'\.ucf-new', 'ext'); - self.add_taboo(r'\.cfsaved', 'ext'); - self.add_taboo(r'\.rhn-cfg-tmp-*', 'ext'); - - self.config_files = {} - ''' - @ivar: dict with all called and included configuration files - to avoid double including - @type: dict - ''' - - self.config_was_read = False - ''' - @ivar: flag whether the configuration file was read. - @type: bool - ''' - - self.config = [] - ''' - @ivar: the configuration, how it was read from cofiguration file(s) - @type: list - ''' - - self.scripts = {} - ''' - @ivar: dict of LogRotateScript objects - with all named scripts found in configuration - @type: dict - ''' - - self.defined_logfiles = {} - ''' - @ivar: all even defined logfiles after globing of file patterns - @type: dict - ''' - - self.logger.debug( _("Logrotate config reader initialised") ) - - #------------------------------------------------------------ - def __str__(self): - ''' - Typecasting function for translating object structure - into a string - - @return: structure as string - @rtype: str - ''' - - pp = pprint.PrettyPrinter(indent=4) - structure = self.as_dict() - return pp.pformat(structure) - - #------------------------------------------------------- - def as_dict(self): - ''' - Transforms the elements of the object into a dict - - @return: structure as dict - @rtype: dict - ''' - - res = { - 'config': self.config, - 'config_file': self.config_file, - 'config_files': self.config_files, - 'config_was_read': self.config_was_read, - 'default': self.default, - 'defined_logfiles': self.defined_logfiles, - 'global_option': self.global_option, - 'logger': self.logger, - 'local_dir': self.local_dir, - 'new_log': self.new_log, - 'search_path': self.search_path, - 'scripts': {}, - 'shred_command': self.shred_command, - 't': self.t, - 'taboo': self.taboo, - 'test_mode': self.test_mode, - 'verbose': self.verbose, - } - - for script_name in self.scripts.keys(): - res['scripts'][script_name] = self.scripts[script_name].as_dict() - - return res - - #------------------------------------------------------------ - def _reset_defaults(self): - ''' - Resetting self.default to the hard coded values - ''' - - _ = self.t.lgettext - - if self.verbose > 3: - self.logger.debug( _("Resetting default values for directives to hard coded values")) - - self.default = {} - - self.default['compress'] = False - self.default['compresscmd'] = 'internal_gzip' - self.default['compressext'] = None - self.default['compressoptions'] = None - self.default['copy'] = False - self.default['copytruncate'] = False - self.default['create'] = { - 'enabled': False, - 'mode': None, - 'owner': None, - 'group': None, - } - self.default['period'] = 7 - self.default['dateext'] = False - self.default['datepattern'] = '%Y-%m-%d' - self.default['delaycompress'] = None - self.default['extension'] = "" - self.default['ifempty'] = True - self.default['mailaddress'] = None - self.default['mailfirst'] = None - self.default['maxage'] = None - self.default['missingok'] = False - self.default['olddir'] = { - 'dirname': '', - 'dateformat': False, - 'enabled': False, - 'mode': None, - 'owner': None, - 'group': None, - } - self.default['rotate'] = 4 - self.default['sharedscripts'] = False - self.default['shred'] = False - self.default['size'] = None - self.default['start'] = 0 - - #------------------------------------------------------------ - def add_taboo(self, pattern, pattern_type = 'file'): - ''' - Add a pattern to the list of taboo patterns self.taboo - Raises a general exception, if pattern_type is invalid - - @param pattern: The patten to append to the taboo list - @type pattern: str - @param pattern_type: The type of the taboo pattern - ('ext', 'file' or 'prefix') - @type pattern_type: str - - @return: None - ''' - - _ = self.t.lgettext - - if not pattern_type in pattern_types: - raise Exception( _("Invalid taboo pattern type '%s' given") % (pattern_type) ) - - pattern = ( pattern_types[pattern_type] % pattern ) - if self.verbose > 3: - self.logger.debug( _("New taboo pattern: '%s'.") % (pattern) ) - - self.taboo.append(pattern) - - #------------------------------------------------------------ - def _init_search_path(self): - ''' - Initialises the internal list of search pathes - - @return: None - ''' - - _ = self.t.lgettext - dir_included = {} - - # Including default path list from environment $PATH - def_path = os.environ['PATH'] - if not def_path: - def_path = '' - sep = os.pathsep - path_list = [] - for item in def_path.split(sep): - if item: - if os.path.isdir(item): - real_dir = os.path.abspath(item) - if not real_dir in dir_included: - path_list.append(real_dir) - dir_included[real_dir] = True - else: - self.logger.debug( _("'%s' is not a directory") % (item)) - - # Including default path list from python - def_path = os.defpath - for item in def_path.split(sep): - if item: - if os.path.isdir(item): - real_dir = os.path.abspath(item) - if not real_dir in dir_included: - path_list.append(real_dir) - dir_included[real_dir] = True - else: - self.logger.debug( _("'%s' is not a directory") % (item)) - - # Including own defined directories - for item in ('/usr/local/bin', '/sbin', '/usr/sbin', '/usr/local/sbin'): - if os.path.isdir(item): - real_dir = os.path.abspath(item) - if not real_dir in dir_included: - path_list.append(real_dir) - dir_included[real_dir] = True - else: - self.logger.debug( _("'%s' is not a directory") % (item)) - - self.search_path = path_list - - #------------------------------------------------------------ - def _get_std_search_path(self, include_current = False): - ''' - Returns a list with all search directories from $PATH and some additionally - directiories. - - @param include_current: include the current working directory - at the end of the list - @type include_current: bool - - @return: list of search directories - @rtype: list - ''' - - #_ = self.t.lgettext - - path_list = self.search_path - if include_current: - item = os.getcwd() - real_dir = os.path.abspath(item) - path_list.append(real_dir) - - return path_list - - #------------------------------------------------------------ - def check_shred_command(self): - ''' - Checks the availability of a check command. Sets self.shred_command to - this system command or to None, if not found (including a warning). - ''' - - _ = self.t.lgettext - path_list = self._get_std_search_path(True) - - cmd = None - found = False - for search_dir in path_list: - if os.path.isdir(search_dir): - cmd = os.path.join(search_dir, 'shred') - if not os.path.isfile(cmd): - continue - if os.access(cmd, os.X_OK): - found = True - break - else: - self.logger.debug( _("Search path '%s' doesn't exists or is not a directory") % (search_dir)) - - if found: - self.logger.debug( _("Shred command found: '%s'") %(cmd) ) - self.shred_command = cmd - return True - else: - self.logger.warning( _("Shred command not found, shred disabled") ) - self.shred_command = None - return False - - #------------------------------------------------------------ - def check_compress_command(self, command): - ''' - Checks the availability of the given compress command. - - 'internal_zip, 'internal_gzip' and 'internal_bzip2' are accepted as - valid compress commands for compressing with the appropriate python modules. - - @param command: command to validate (absolute or relative for - searching in standard search path) - @type command: str - - @return: absolute path of the compress command, 'internal_gzip', - 'internal_bzip2' or None if not found or invalid - @rtype: str or None - ''' - - _ = self.t.lgettext - path_list = self._get_std_search_path(True) - - match = re.search(r'^\s*internal[\-_\s]?zip\s*', command, re.IGNORECASE) - if match: - return 'internal_zip' - - match = re.search(r'^\s*internal[\-_\s]?gzip\s*', command, re.IGNORECASE) - if match: - return 'internal_gzip' - - match = re.search(r'^\s*internal[\-_\s]?bzip2\s*', command, re.IGNORECASE) - if match: - return 'internal_bzip2' - - if os.path.isabs(command): - if os.access(command, os.X_OK): - return os.path.abspath(command) - else: - return None - - cmd = None - found = False - for search_dir in path_list: - if os.path.isdir(search_dir): - cmd = os.path.join(search_dir, command) - if not os.path.isfile(cmd): - continue - if os.access(cmd, os.X_OK): - found = True - break - else: - self.logger.debug( _("Search path '%s' doesn't exists or is not a directory") % (search_dir)) - - if found: - return os.path.abspath(cmd) - else: - return None - - #------------------------------------------------------------ - def get_config(self): - ''' - Returns the configuration, how it was read from configuration file(s) - - @return: configuration - @rtype: dict or None - ''' - - if not self._read_main_configfile(): - return None - - return self.config - - #------------------------------------------------------------ - def get_scripts(self): - ''' - Returns the scriptlist, how it was read from configuration file(s) - - @return: list of scripts - @rtype: list - ''' - - if not self._read_main_configfile(): - return None - - return self.scripts - - #------------------------------------------------------------ - def _read_main_configfile(self): - ''' - Reads the main configuration file (self.config_file). - - @return: success of reading - @rtype: bool - ''' - - _ = self.t.lgettext - - if self.config_was_read: - return True - - if not os.path.exists(self.config_file): - raise LogrotateConfigurationError( _("File '%s' doesn't exists.") % (self.config_file)) - - self.config_file = os.path.abspath(self.config_file) - - if not self._read(self.config_file): - return None - - self.config_was_read = True - return True - - #------------------------------------------------------------ - def _read(self, configfile): - ''' - Reads the configuration from given configuration file and all - included files. - - @param configfile: the configfile to read - @type configfile: str - ''' - - _ = self.t.lgettext - pp = pprint.PrettyPrinter(indent=4) - self.logger.debug( _("Try reading configuration from '%s' ...") % (configfile) ) - - if not os.path.exists(configfile): - raise LogrotateConfigurationError( _("File '%s' doesn't exists.") % (configfile)) - - if not os.path.isfile(configfile): - raise LogrotateConfigurationError( _("'%s' is not a regular file.") % (configfile)) - - self.config_files[configfile] = True - - self.logger.info( _("Reading configuration from '%s' ...") % (configfile) ) - - cfile = None - try: - cfile = open(configfile, 'Ur') - except IOError, e: - raise LogrotateConfigurationError( ( _("Could not read configuration file '%s'") % (configfile) ) + ': ' + str(e)) - lines = cfile.readlines() - cfile.close() - - # defaults for the big loop - linenr = 0 - in_fd = False - in_script = False - in_logfile_list = False - lastrow = '' - newscript = '' - - # inspect every line of configuration file - for line in lines: - - linenr += 1 - line = line.strip() - - # Perform a backslash at the end of the line - line = lastrow + line - match = re.search(r'\\$', line) - if match: - line = re.sub(r'\\$', '', line) - lastrow = line - continue - lastrow = '' - - # delete comments - line = re.sub(r'^#.*', '', line) - if line == '': - continue - - # perform script content - if in_script: - match = re.search(r'^endscript$', line) - if match: - in_script = False - continue - #self.scripts[newscript]['cmd'].append(line) - self.scripts[newscript].add_cmd(line) - continue - - # start of a logfile definition - if line == '{': - - if self.verbose > 3: - self.logger.debug( ( _("Starting a logfile definition (file '%(file)s', line %(line)s)") - % {'file': configfile, 'line': linenr})) - - self._start_logfile_definition( - line = line, - filename = configfile, - in_fd = in_fd, - in_logfile_list = in_logfile_list, - linenr = linenr - ) - in_fd = True - in_logfile_list = False - continue - - # start of a logfile pattern - match = re.search(r'^[\'"]', line) - if match or os.path.isabs(line): - - if in_fd: - raise LogrotateConfigurationError( - ( _("Logfile pattern definition not allowed inside a logfile definition (file '%(file)s', line %(line)s)") - % {'file': configfile, 'line': linenr}) - ) - do_start_logfile_definition = False - - # look, whether a start of a logfile definition is necessary - match_bracket = re.search(r'\s*{\s*$', line) - if match_bracket: - line = re.sub(r'\s*{\s*$', '', line) - do_start_logfile_definition = True - if not in_logfile_list: - self._start_new_log(configfile, linenr) - in_logfile_list = True - - parts = split_parts(line) - if self.verbose > 3: - self.logger.debug( - ( _("Split into parts of: '%s'") % (line)) - + ":\n" + pp.pformat(parts) - ) - - for pattern in parts: - if pattern == '{': - raise LogrotateConfigurationError( - ( _("Syntax error: open curly bracket inside a logfile pattern definition (file '%(file)s', line %(line)s)") - % {'file': configfile, 'line': linenr}) - ) - self.new_log['file_patterns'].append(pattern) - - # start of a logfile definition, if necessary - if do_start_logfile_definition: - self._start_logfile_definition( - line = line, - filename = configfile, - in_fd = in_fd, - in_logfile_list = in_logfile_list, - linenr = linenr - ) - in_fd = True - in_logfile_list = False - - continue - - # end of a logfile definition - match = re.search(r'^}(.*)', line) - if match: - if not in_fd: - raise LogrotateConfigurationError( - ( _("Syntax error: unbalanced closing curly bracket found (file '%(file)s', line %(line)s)") - % {'file': configfile, 'line': linenr}) - ) - rest = match.group(1) - if self.verbose > 2: - self.logger.debug( ( _("End of a logfile definition (file '%(file)s', line %(line)s)") % {'file': configfile, 'line': linenr})) - if rest: - self.logger.warning( - ( _("Needless content found at the end of a logfile definition found: '%(rest)s' (file '%(file)s', line %(line)s)") - % { 'rest': str(rest), 'file': configfile, 'line': linenr}) - ) - # set a compress ext, if Compress is True - if self.new_log['compress']: - if not self.new_log['compressext']: - if self.new_log['compresscmd'] == 'internal_gzip': - self.new_log['compressext'] = '.gz' - elif self.new_log['compresscmd'] == 'internal_zip': - self.new_log['compressext'] = '.zip' - elif self.new_log['compresscmd'] == 'internal_bzip2': - self.new_log['compressext'] = '.bz2' - else: - msg = _("No extension for compressed logfiles given " + - "(File of definition: '%(file)s', start definition: %(rownum)d).") \ - % { 'file': self.new_log['configfile'], 'rownum': self.new_log['configrow']} - raise LogrotateConfigurationError(msg) - # set ifempty => True, if a minsize was given - if self.new_log['size']: - self.new_log['ifempty'] = False - found_files = self._assign_logfiles() - if self.verbose > 3: - self.logger.debug( ( _("New logfile definition:") + "\n" + pp.pformat(self.new_log))) - if found_files > 0: - if self.new_log['postrotate']: - script = self.new_log['postrotate'] - if self.scripts[script]: - self.scripts[script].post_files += found_files - else: - msg = _("Postrotate script '%s' not found.") % (script) - self.logger.error(msg) - if self.new_log['lastaction']: - script = self.new_log['lastaction'] - if self.scripts[script]: - self.scripts[script].last_files += found_files - else: - msg = _("Lastaction script '%s' not found.") % (script) - self.logger.error(msg) - self.config.append(self.new_log) - in_fd = False - in_logfile_list = False - - continue - - # performing includes - match = re.search(r'^include(?:\s+(.*))?$', line, re.IGNORECASE) - if match: - rest = match.group(1) - if in_fd or in_logfile_list: - self.logger.warning( - ( _("Syntax error: include may not appear inside of log file definition (file '%(file)s', line %(line)s)") - % {'file': configfile, 'line': linenr}) - ) - continue - self._do_include(line, rest, configfile, linenr) - continue - - # start of a (regular) script definition - pattern = r'^(' + '|'.join(script_directives) + r')(\s+.*)?$' - match = re.search(pattern, line, re.IGNORECASE) - if match: - script_type = match.group(1).lower() - script_name = None - if match.group(2) is not None: - values = split_parts(match.group(2)) - if values[0]: - script_name = values[0] - if self.verbose > 3: - self.logger.debug( - ( _("Found start of a regular script definition: type: '%(type)s', name: '%(name)s' (file '%(file)s', line %(line)s)") - % {'type': script_type, 'name': script_name, 'file': configfile, 'line': linenr}) - ) - newscript = self._start_log_script_definition( - script_type = script_type, - script_name = script_name, - line = line, - filename = configfile, - in_fd = in_fd, - linenr = linenr, - ) - if newscript: - in_script = True - if self.verbose > 3: - self.logger.debug( ( _("New log script name: '%s'.") % (newscript) )) - continue - - # start of an explicite external script definition - match = re.search(r'^script(\s+.*)?$', line, re.IGNORECASE) - if match: - if self.verbose > 3: - self.logger.debug( ( _("Found start of a external script definition. (file '%(file)s', line %(line)s)") - % {'file': configfile, 'line': linenr}) - ) - rest = match.group(1) - if in_fd or in_logfile_list: - self.logger.warning( - ( _("Syntax error: external script definition may not appear inside of a log file definition (file '%(file)s', line %(line)s)") - % {'file': configfile, 'line': linenr}) - ) - continue - newscript = self._ext_script_definition( - line, rest, configfile, linenr - ) - if newscript: - in_script = True - if self.verbose > 3: - self.logger.debug( ( _("New external script name: '%s'.") % (newscript) )) - continue - - # all other options - if not self._option(line, in_fd, configfile, linenr): - self.logger.warning( ( _("Syntax error in file '%(file)s', line %(line)s") - % {'file': configfile, 'line': linenr}) - ) - - return True - - #------------------------------------------------------------ - def _option(self, line, in_fd, filename, linenr): - ''' - Checks the given line as a logrotate option and assign this - option on success to the default options or in the current - logfile directive - - @param line: line of current config file - @type line: str - @param in_fd: parsing inside a logfile definition - @type in_fd: bool - @param filename: current configuration file - @type filename: str - @param linenr: current line number of configuration file - @type linenr: int - - @return: success of parsing this option - @rtype: bool - ''' - - _ = self.t.lgettext - if self.verbose > 4: - self.logger.debug( - ( _("Checking line '%(line)s' for a logrotate option. (file '%(file)s', line %(lnr)s)") - % {'line': line, 'file': filename, 'lnr': linenr}) - ) - - # where to insert the option? - directive = self.default - directive_str = 'default' - if in_fd: - directive = self.new_log - directive_str = 'new_log' - - # extract option from line - option = None - val = None - match = re.search(r'^(\S+)\s*(.*)', line) - if match: - option = match.group(1).lower() - val = match.group(2) - else: - self.logger.warning( ( _("Could not detect option in line '%s'.") % (line))) - return False - val = re.sub(r'^\s+$', '', val) - if self.verbose > 4: - msg = _("Found option '%(opt)s' with value '%(val)s'.") \ - % {'opt': option, 'val': val} - self.logger.debug(msg) - - # Check for unsupported options - pattern = r'^(' + '|'.join(unsupported_options) + r')$' - match = re.search(pattern, option, re.IGNORECASE) - if match: - self.logger.info( - ( _("Unsupported option '%(option)s'. (file '%(file)s', line %(lnr)s)") - % {'option': match.group(1).lower(), 'file': filename, 'lnr': linenr}) - ) - return True - - # Check for boolean option - pattern = r'^(not?)?(' + '|'.join(boolean_options) + r')$' - match = re.search(pattern, option, re.IGNORECASE) - if match: - negated = match.group(1) - key = match.group(2).lower() - if val: - self.logger.warning( - ( _("Found value '%(value)s' behind the boolean option '%(option)s', ignoring. (file '%(file)s', line %(lnr)s)") - % {'value': val, 'option': option, 'file': filename, 'lnr': linenr}) - ) - if negated is None: - option_value = True - else: - option_value = False - if self.verbose > 4: - self.logger.debug( - ( _("Setting boolean option '%(option)s' in '%(directive)s' to '%(value)s'. (file '%(file)s', line %(lnr)s)") - % {'option': key, 'directive': directive_str, 'value': str(option_value), 'file': filename, 'lnr': linenr}) - ) - directive[key] = option_value - if key == 'copy' and option_value: - if directive['copytruncate']: - msg = _("Option 'copy' disables option 'copytruncate'. (file '%(file)s', line %(lnr)s)") \ - % {'file': filename, 'lnr': linenr} - self.logger.warning(msg) - directive['copytruncate'] = False - if directive['create']['enabled']: - msg = _("Option 'copy' disables option 'create'. (file '%(file)s', line %(lnr)s)") \ - % {'file': filename, 'lnr': linenr} - self.logger.warning(msg) - directive['create']['enabled'] = False - elif key == 'copytruncate' and option_value: - if directive['copy']: - msg = _("Option 'copytruncate' disables option 'copy'. (file '%(file)s', line %(lnr)s)") \ - % {'file': filename, 'lnr': linenr} - self.logger.warning(msg) - directive['copy'] = False - if directive['create']['enabled']: - msg = _("Option 'copytruncate' disables option 'create'. (file '%(file)s', line %(lnr)s)") \ - % {'file': filename, 'lnr': linenr} - self.logger.warning(msg) - directive['create']['enabled'] = False - return True - - # Check for integer options - pattern = r'^(not?)?(' + '|'.join(integer_options) + r')$' - match = re.search(pattern, option, re.IGNORECASE) - if match: - negated = match.group(1) - key = match.group(2).lower() - option_value = 0 - if negated is None: - if key in options_with_values: - if val is None or val == '': - self.logger.warning( ( _("Option '%s' without a necessary value.") % (key))) - return False - else: - if val is None or val == '': - val = '1' - try: - option_value = long(val) - except ValueError, e: - self.logger.warning( - ( _("Option '%(option)s' has no integer value: %(msg)s.") - % {'option': key, 'msg': str(e)}) - ) - return False - if option_value < 0: - self.logger.warning( - ( _("Negative value %(value)s for option '%(option)s' is not allowed.") - % {'value': str(option_value), 'option': key}) - ) - return False - if self.verbose > 4: - self.logger.debug( - ( _("Setting integer option '%(option)s' in '%(directive)s' to '%(value)s'. (file '%(file)s', line %(lnr)s)") - % {'option': key, 'directive': directive_str, 'value': str(option_value), 'file': filename, 'lnr': linenr}) - ) - directive[key] = option_value - return True - - # Check for mail address - match = re.search(r'^(not?)?mail$', option, re.IGNORECASE) - if match: - negated = match.group(1) - if negated: - directive['mailaddress'] = None - if val is not None and val != '': - self.logger.warning( - ( _("Senseless option value '%(value)s' after '%(option)s'.") - % {'value': val, 'option': option.lower()}) - ) - return False - return True - address_list = get_address_list(val, self.verbose) - if len(address_list): - directive['mailaddress'] = address_list - else: - directive['mailaddress'] = None - if self.verbose > 4: - pp = pprint.PrettyPrinter(indent=4) - msg = _("Setting mail address in '%(directive)s' to '%(addr)s'. (file '%(file)s', line %(lnr)s)") \ - % { - 'directive': directive_str, - 'addr': pp.pformat(directive['mailaddress']), - 'file': filename, - 'lnr': linenr, - } - self.logger.debug(msg) - return True - - # Check for mailfirst/maillast - match = re.search(r'^mail(first|last)$', option, re.IGNORECASE) - if match: - when = match.group(1).lower() - option_value = False - if when == 'first': - option_value = True - directive['mailfirst'] = option_value - if self.verbose > 4: - self.logger.debug( - ( _("Setting mailfirst in '%(directive)s' to '%(value)s'. (file '%(file)s', line %(lnr)s)") - % {'directive': directive_str, 'value': str(option_value), 'file': filename, 'lnr': linenr}) - ) - if val is not None and val != '': - self.logger.warning( - ( _("Senseless option value '%(value)s' after '%(option)s'.") - % {'value': val, 'option': option.lower()}) - ) - return False - return True - - # Check for string options - pattern = r'^(' + '|'.join(string_options) + r')$' - match = re.search(pattern, option, re.IGNORECASE) - if match: - key = match.group(1).lower() - if key in options_with_values: - if self.verbose > 5: - self.logger.debug( ( _("Option '%s' must have a value.") %(key))) - if (val is None) or (val == ''): - self.logger.warning( ( _("Option '%s' without a value") %(key))) - return False - if key == 'compresscmd': - prog = self.check_compress_command(val) - if prog is None: - self.logger.warning( ( _("Compress command '%s' not found.") %(val))) - return False - val = prog - if key == 'compressoptions' and val is None: - val = '' - directive[key] = val - return True - - # Check for global options - pattern = r'^(' + '|'.join(global_options) + r')$' - match = re.search(pattern, option, re.IGNORECASE) - if match: - key = match.group(1).lower() - if in_fd: - self.logger.warning( ( _("Option '%s' not allowed inside a logfile directive.") %(key))) - return False - if key in options_with_values: - if self.verbose > 5: - self.logger.debug( ( _("Option '%s' must have a value.") %(key))) - if (val is None) or (re.search(r'^\s*$', val) is not None): - self.logger.warning( ( _("Option '%s' without a value") %(key))) - return False - if key in path_options: - if not os.path.abspath(val): - self.logger.warning( - ( _("Value '%(value)s' for option '%(option)s' is not an absolute path.") - % {'value': val, 'option': key} ) - ) - return False - if key == 'mailfrom': - pair = email.utils.parseaddr(val) - if not email_valid(pair[1]): - msg = _("Invalid mail address for 'mailfrom' given: '%s'.") % (val) - self.logger.warning(msg) - return False - val = pair - elif key == 'smtpport': - port = 25 - try: - port = int(val) - except ValueError, e: - msg = _("Invalid SMTP port '%s' given.") % (val) - self.logger.warning(msg) - return False - if port < 1 or port >= 2**15: - msg = _("Invalid SMTP port '%s' given.") % (val) - self.logger.warning(msg) - return False - val = port - elif key == 'smtptls': - use_tls = False - match = re.search(r'^\s*(?:0+|false|no?)\s*$', val, re.IGNORECASE) - if not match: - match = re.search(r'^\s*(?:1|true|y(?:es)?)\s*$', val, re.IGNORECASE) - if match: - use_tls = True - else: - use_tls = bool(val) - val = use_tls - if self.verbose > 4: - self.logger.debug( - ( _("Setting global option '%(option)s' to '%(value)s'. (file '%(file)s', line %(lnr)s)") - % {'option': key, 'directive': directive_str, 'value': str(val), 'file': filename, 'lnr': linenr}) - ) - self.global_option[key] = val - return True - - # Check for rotation period - pattern = r'^(' + '|'.join(valid_periods.keys()) + r'|period)$' - match = re.search(pattern, option, re.IGNORECASE) - if match: - key = match.group(1).lower() - if self.verbose > 4: - self.logger.debug( - ( _("Checking 'period': key '%(key)s', value '%(value)s'. (file '%(file)s', line %(lnr)s)") - % {'key': key, 'value': str(val), 'file': filename, 'lnr': linenr}) - ) - option_value = 1 - if key in valid_periods: - if (val is not None) and (re.search(r'^\s*$', val) is None): - self.logger.warning( - ( _("Option '%(option)s' may not have a value ('%(value)s'). (file '%(file)s', line %(lnr)s)") - % {'option': key, 'value': val, 'file': filename, 'lnr': linenr}) - ) - option_value = valid_periods[key] - else: - try: - option_value = period2days(val, verbose = self.verbose) - except ValueError, e: - self.logger.warning( ( _("Invalid period definition: '%s'") %(val) )) - return False - if self.verbose > 4: - self.logger.debug( - ( _("Setting 'period' in '%(directive)s' to %(days)f days. (file '%(file)s', line %(lnr)s)") - % {'directive': directive_str, 'days': option_value, 'file': filename, 'lnr': linenr}) - ) - directive['period'] = option_value - return True - - # get maximum age of old rotated log files - match = re.search(r'^(not?)?maxage$', option, re.IGNORECASE) - if match: - negated = False - if match.group(1) is not None: - negated = True - if (val is None) or re.search(r'^\s*$', val) is not None: - negated = True - option_value = 0 - if not negated: - try: - option_value = period2days(val, verbose = self.verbose) - except ValueError, e: - self.logger.warning( ( _("Invalid maxage definition: '%s'") %(val) )) - return False - if self.verbose > 4: - self.logger.debug( - ( _("Setting 'maxage' in '%(directive)s' to %(days)f days. (file '%(file)s', line %(lnr)s)") - % {'directive': directive_str, 'days': option_value, 'file': filename, 'lnr': linenr}) - ) - directive['maxage'] = option_value - return True - - # Setting date extension of rotated log files - match = re.search(r'^(no)?dateext$', option, re.IGNORECASE) - if match: - - negated = False - if match.group(1) is not None: - negated = True - use_dateext = False - dateext = None - - if self.verbose > 4: - self.logger.debug( - ( _("Checking 'dateext', negated: '%(negated)s'. (file '%(file)s', line %(lnr)s)") - % {'negated': str(negated), 'file': filename, 'lnr': linenr}) - ) - values = [] - if val is not None: - values = split_parts(val) - - if not negated: - first_val = '' - if len(values) > 0: - first_val = values[0].lower() - option_value = first_val - if first_val is None or \ - re.search(r'^\s*$', first_val) is not None: - option_value = 'true' - if self.verbose > 5: - self.logger.debug( - ( _("'dateext': first_val: '%(first_val)s', option_value: '%(value)s'. (file '%(file)s', line %(lnr)s)") - % {'first_val': first_val, 'value': option_value, 'file': filename, 'lnr': linenr}) - ) - if option_value in yes_values: - use_dateext = True - elif option_value in no_values: - use_dateext = False - else: - use_dateext = True - dateext = val - - if self.verbose > 4: - self.logger.debug( - ( _("Setting 'dateext' in '%(directive)s' to %(ext)s. (file '%(file)s', line %(lnr)s)") - % {'directive': directive_str, 'ext': str(use_dateext), 'file': filename, 'lnr': linenr}) - ) - directive['dateext'] = use_dateext - - if dateext is not None: - if self.verbose > 4: - self.logger.debug( - ( _("Setting 'datepattern' in '%(directive)s' to '%(pattern)s'. (file '%(file)s', line %(lnr)s)") - % {'directive': directive_str, 'pattern': dateext, 'file': filename, 'lnr': linenr}) - ) - directive['datepattern'] = dateext - - return True - - # Checking for create options ... - match = re.search(r'(not?)?create$', option, re.IGNORECASE) - if match: - - negated = False - if match.group(1) is not None: - negated = True - - if self.verbose > 5: - self.logger.debug( - ( _("Checking for 'create' ... (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - - if negated: - if self.verbose > 4: - self.logger.debug( - ( _("Removing 'create'. (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - directive['create']['enabled'] = False - return True - - if directive['copy']: - msg = _("Option 'copy' was set, so option 'create' has no effect. (file '%(file)s', line %(lnr)s)") \ - % {'file': filename, 'lnr': linenr} - self.logger.warning(msg) - directive['create']['enabled'] = False - return True - - if directive['copytruncate']: - msg = _("Option 'copytruncate' was set, so option 'create' has no effect. (file '%(file)s', line %(lnr)s)") \ - % {'file': filename, 'lnr': linenr} - self.logger.warning(msg) - directive['create']['enabled'] = False - return True - - values = [] - if val is not None: - values = split_parts(val) - - directive['create']['enabled'] = True - - mode = None - owner = None - group = None - - # Check for create mode - if len(values) > 0: - if self.verbose > 5: - self.logger.debug( - ( _("Trying to determine create mode '%(mode)s'... (file '%(file)s', line %(lnr)s)") - % {'mode': values[0], 'file': filename, 'lnr': linenr}) - ) - mode_octal = values[0] - if re.search(r'^0', mode_octal) is None: - mode_octal = '0' + mode_octal - try: - mode = int(mode_octal, 8) - except ValueError: - self.logger.warning( ( _("Invalid create mode '%s'.") %(values[1]))) - return False - - # Check for Owner (user, uid) - if len(values) > 1: - owner_raw = values[1] - if self.verbose > 5: - self.logger.debug( - ( _("Trying to determine create owner '%(owner)s'... (file '%(file)s', line %(lnr)s)") - % {'owner': owner_raw, 'file': filename, 'lnr': linenr}) - ) - if re.search(r'^[1-9]\d*$', owner_raw) is not None: - owner = int(owner_raw) - else: - try: - owner = pwd.getpwnam(owner_raw)[2] - except KeyError: - self.logger.warning( ( _("Invalid owner '%s' in 'create'.") %(owner_raw))) - return False - - # Check for Group (gid) - if len(values) > 2: - group_raw = values[2] - if self.verbose > 5: - self.logger.debug( - ( _("Trying to determine create group '%(group)s'... (file '%(file)s', line %(lnr)s)") - % {'group': group_raw, 'file': filename, 'lnr': linenr}) - ) - if re.search(r'^[1-9]\d*$', group_raw) is not None: - group = int(group_raw) - else: - try: - group = grp.getgrnam(group_raw)[2] - except KeyError: - self.logger.warning( ( _("Invalid group '%s' in 'create'.") %(group_raw))) - return False - - # Give values back ... - directive['create']['mode'] = mode - directive['create']['owner'] = owner - directive['create']['group'] = group - return True - - # checking for olddir ... - match = re.search(r'^(not?)?olddir$', option, re.IGNORECASE) - if match: - - negated = False - if match.group(1) is not None: - negated = True - - if self.verbose > 5: - self.logger.debug( - ( _("Checking for 'olddir' ... (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - - if negated: - if self.verbose > 4: - self.logger.debug( - ( _("Removing 'olddir'. (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - directive['olddir']['enabled'] = False - return True - - values = [] - if val is not None: - values = split_parts(val) - - # Check for dirname of olddir - if len(values) < 1 or values[0] is None or re.search(r'^\s*$', values[0]) is not None: - self.logger.warning( ( _("Option 'olddir' without a value given."))) - return False - directive['olddir']['dirname'] = values[0] - directive['olddir']['enabled'] = True - - mode = None - owner = None - group = None - - # Check for create mode of olddir - if len(values) > 1: - if self.verbose > 5: - self.logger.debug( - ( _("Trying to determine olddir create mode '%(mode)s' ... (file '%(file)s', line %(lnr)s)") - % {'mode': values[1], 'file': filename, 'lnr': linenr}) - ) - mode_octal = values[1] - if re.search(r'^0', mode_octal) is None: - mode_octal = '0' + mode_octal - try: - mode = int(mode_octal, 8) - except ValueError: - self.logger.warning( ( _("Invalid create mode '%s' in 'olddir'.") %(values[1]))) - return False - - # Check for Owner (user, uid) - if len(values) > 2: - owner_raw = values[2] - if self.verbose > 5: - self.logger.debug( - ( _("Trying to determine olddir owner '%(owner)s' ... (file '%(file)s', line %(lnr)s)") - % {'owner': owner_raw, 'file': filename, 'lnr': linenr}) - ) - if re.search(r'^[1-9]\d*$', owner_raw) is not None: - owner = int(owner_raw) - else: - try: - owner = pwd.getpwnam(owner_raw)[2] - except KeyError: - self.logger.warning( ( _("Invalid owner '%s' in 'olddir'.") %(owner_raw))) - return False - - # Check for Group (gid) - if len(values) > 3: - group_raw = values[3] - if self.verbose > 5: - self.logger.debug( - ( _("Trying to determine olddir group '%(group)s' ... (file '%(file)s', line %(lnr)s)") - % {'group': group_raw, 'file': filename, 'lnr': linenr}) - ) - if re.search(r'^[1-9]\d*$', group_raw) is not None: - group = int(group_raw) - else: - try: - group = grp.getgrnam(group_raw)[2] - except KeyError: - self.logger.warning( ( _("Invalid group '%s' in 'olddir'.") %(group_raw))) - return False - - # Give values back ... - directive['olddir']['mode'] = mode - directive['olddir']['owner'] = owner - directive['olddir']['group'] = group - return True - - # Check for minimum size for ratation - match = re.search(r'^size(?:(?:\s*=|\s)|$)', line, re.IGNORECASE) - if match: - size_str = re.sub(r'^size(?:\s*=\s*|\s+)', '', line) - if self.verbose > 5: - self.logger.debug( - ( _("Checking for option 'size', value: '%(value)s' ... (file '%(file)s', line %(lnr)s)") - % {'value': size_str, 'file': filename, 'lnr': linenr}) - ) - if size_str is None: - self.logger.warning( _("Failing size definition.")) - return False - size_bytes = None - try: - size_bytes = human2bytes(size_str, verbose = self.verbose) - except ValueError, e: - self.logger.warning( ( _("Invalid definition for 'size': '%s'.") %(size_str))) - return False - if self.verbose > 4: - self.logger.debug( - ( _("Got a rotation size in '%(directive)s' of %(bytes)d bytes. (file '%(file)s', line %(lnr)s)") - % {'directive': directive_str, 'bytes': size_bytes, 'file': filename, 'lnr': linenr}) - ) - directive['size'] = size_bytes - return True - - # Check for taboo options - pattern = r'^taboo(ext|file|prefix)$' - match = re.search(pattern, option, re.IGNORECASE) - if match: - key = match.group(1).lower() - if self.verbose > 5: - self.logger.debug( - ( _("Checking for option 'taboo%(type)s', value: '%(value)s' ... (file '%(file)s', line %(lnr)s)") - % {'type': key, 'value': val, 'file': filename, 'lnr': linenr}) - ) - - if in_fd: - self.logger.warning( ( _("Option 'taboo%s' not allowed inside a logfile directive.") %(key))) - return False - - values = [] - if val is not None: - values = split_parts(val) - - extend = False - if len(values) > 0 and values[0] is not None and values[0] == '+': - extend = True - values.pop(0) - - if len(values) < 1: - self.logger.warning( ( _("Option 'taboo%s' needs a value.") %(key))) - return False - - if not extend: - self.taboo = [] - for extension in values: - self.add_taboo(extension, key) - - return True - - # Option not found, I'm angry - self.logger.warning( ( _("Unknown option '%s'.") %(option))) - return False - - #------------------------------------------------------------ - def _ext_script_definition(self, line, rest, filename, linenr): - ''' - Starts a new explicite external script definition. - It raises a LogrotateConfigurationError on error. - - @param line: line of current config file - @type line: str - @param rest: rest of the current line after »script« - @type rest: str - @param filename: current configuration file - @type filename: str - @param linenr: current line number of configuration file - @type linenr: int - - @return: name of the script (if a new script definition) or None - @rtype: str or None - ''' - - _ = self.t.lgettext - - # split the rest in chunks - values = split_parts(rest) - - # insufficient arguments to include ... - if len(values) < 1: - self.logger.warning( - ( _("No script name given in a script directive. (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - return None - - # to much arguments to include ... - if len(values) > 1: - self.logger.warning( - ( _("Only one script name is allowed in a script directive, the first one is used. (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - - script_name = values[0] - - if script_name in self.scripts: - self.logger.warning( - ( _("Script name '%(name)s' is allready declared, it will be overwritten. (file '%(file)s', line %(lnr)s)") - % {'name': script_name, 'file': filename, 'lnr': linenr}) - ) - - self.scripts[script_name] = LogRotateScript( - name = script_name, - local_dir = self.local_dir, - verbose = self.verbose, - test_mode = self.test_mode, - ) - #self.scripts[script_name]['cmd'] = [] - #self.scripts[script_name]['post_files'] = 0 - #self.scripts[script_name]['last_files'] = 0 - #self.scripts[script_name]['first'] = False - #self.scripts[script_name]['prerun'] = False - #self.scripts[script_name]['donepost'] = False - #self.scripts[script_name]['donelast'] = False - - return script_name - - #------------------------------------------------------------ - def _do_include( self, line, rest, filename, linenr): - ''' - Starts a new logfile definition. - It raises a LogrotateConfigurationError on error. - - @param line: line of current config file - @type line: str - @param rest: rest of the current line after »include« - @type rest: str - @param filename: current configuration file - @type filename: str - @param linenr: current line number of configuration file - @type linenr: int - - @return: Success of include - @rtype: bool - ''' - - _ = self.t.lgettext - - # split the rest in chunks - values = split_parts(rest) - - # insufficient arguments to include ... - if len(values) < 1: - self.logger.warning( - ( _("No file or directory given in a include directive (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - return False - - # to much arguments to include ... - if len(values) > 1: - self.logger.warning( - ( _("Only one declaration of a file or directory is allowed in a include directive, the first one is used. (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - - include = values[0] - - # including object doesn't exists - if not os.path.exists(include): - self.logger.warning( - ( _("Including object '%(include)s' doesn't exists. (file '%(file)s', line %(lnr)s)") - % {'include': include, 'file': filename, 'lnr': linenr}) - ) - return False - - include = os.path.abspath(include) - - # including object is neither a regular file nor a directory - if not (os.path.isfile(include) or os.path.isdir(include)): - self.logger.warning( - ( _("Including object '%(include)s' is neither a regular file nor a directory. (file '%(file)s', line %(lnr)s)") - % {'include': include, 'file': filename, 'lnr': linenr}) - ) - return False - - if self.verbose > 1: - self.logger.debug( ( _("Trying to include object '%s' ...") % (include) )) - - # including object is a regular file - if os.path.isfile(include): - if include in self.config_files: - self.logger.warning( - ( _("Recursive including of '%(include)s'. (file '%(file)s', line %(lnr)s)") - % {'include': include, 'file': filename, 'lnr': linenr}) - ) - return False - return self._read(include) - - # This should never happen ... - if not os.path.isdir(include): - raise Exception( - ( _("What the hell is this: '%(include)s'. (file '%(file)s', line %(lnr)s)") - % {'include': include, 'file': filename, 'lnr': linenr}) - ) - - # including object is a directory - include all files - if self.verbose > 1: - self.logger.debug( ( _("Including directory '%s' ...") % (include) )) - - dir_list = os.listdir(include) - for item in sorted(dir_list, key=str.lower): - - item_path = os.path.abspath(os.path.join(include, item)) - if self.verbose > 2: - self.logger.debug( ( _( "Including item '%(item)s' ('%(path)s') ..." ) - % {'item': item, 'path': item_path} ) - ) - - # Skip directories - if os.path.isdir(item_path): - if self.verbose > 1: - self.logger.debug( ( _("Skip subdirectory '%s' in including.") % (item_path))) - continue - - # Skip non regular files - if not os.path.isfile(item_path): - self.logger.debug( ( _("Item '%s' is not a regular file.") % (item_path))) - continue - - # Check for taboo pattern - taboo_found = False - for pattern in self.taboo: - match = re.search(pattern, item) - if match: - if self.verbose > 1: - self.logger.debug( - ( _("Item '%(item)s' is matching pattern '%(pattern)s', skiping.") - % {'item': item, 'pattern': pattern}) - ) - taboo_found = True - break - if taboo_found: - continue - - # Check, whther it was former included - if item_path in self.config_files: - self.logger.warning( - ( _("Recursive including of '%(include)s' (file '%(file)s', line %(lnr)s)") - % {'include': item_path, 'file': filename, 'lnr': linenr}) - ) - return False - self._read(item_path) - - #------------------------------------------------------------ - def _start_logfile_definition( - self, line, filename, in_fd, in_logfile_list, linenr - ): - ''' - Starts a new logfile definition. - It raises a LogrotateConfigurationError on error. - - @param line: line of current config file - @type line: str - @param filename: current configuration file - @type filename: str - @param in_fd: parsing inside a logfile definition - @type in_fd: bool - @param in_logfile_list: logfile pattern list was started - @type in_logfile_list: bool - @param linenr: current line number of configuration file - @type linenr: int - - @return: name of the script (if a new script definition) or None - @rtype: str or None - ''' - - _ = self.t.lgettext - - if in_fd: - raise LogrotateConfigurationError( - ( _("Nested logfile definitions are not allowed. (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - - if not in_logfile_list: - raise LogrotateConfigurationError( - ( _("No logfile pattern defined on starting a logfile definition. (file '%(file)s', line %(lnr)s)") - % {'file': filename, 'lnr': linenr}) - ) - - #------------------------------------------------------------ - def _start_log_script_definition( self, script_type, script_name, line, filename, in_fd, linenr): - ''' - Starts a new logfile definition or logfile refrence - inside a logfile definition. - It raises a LogrotateConfigurationError outside a logfile definition. - - @param script_type: postrotate, prerotate, firstaction - or lastaction - @type script_type: str - @param script_name: name of refernced script - @type script_name: str or None - @param line: line of current config file - @type line: str - @param filename: current configuration file - @type filename: str - @param in_fd: parsing inside a logfile definition - @type in_fd: bool - @param linenr: current line number of configuration file - @type linenr: int - - @return: name of the script (if a new script definition) or None - @rtype: str or None - ''' - - _ = self.t.lgettext - - if not in_fd: - raise LogrotateConfigurationError( - ( _("Directive '%(directive)s' is not allowed outside of a logfile definition. (file '%(file)s', line %(lnr)s)") - % {'directive': script_type, 'file': filename, 'lnr': linenr}) - ) - - if script_name: - self.new_log[script_type] = script_name - return None - - new_script_name = self._new_scriptname(script_type) - - self.scripts[new_script_name] = LogRotateScript( - name = new_script_name, - local_dir = self.local_dir, - verbose = self.verbose, - test_mode = self.test_mode, - ) - #self.scripts[new_script_name] = {} - #self.scripts[new_script_name]['cmd'] = [] - #self.scripts[new_script_name]['post_files'] = 0 - #self.scripts[new_script_name]['last_files'] = 0 - #self.scripts[new_script_name]['first'] = False - #self.scripts[new_script_name]['prerun'] = False - #self.scripts[new_script_name]['donepost'] = False - #self.scripts[new_script_name]['donelast'] = False - - self.new_log[script_type] = new_script_name - - return new_script_name - - #------------------------------------------------------------ - def _new_scriptname(self, script_type = 'script'): - ''' - Retrieves a new, unique script name. - - @param script_type: prefix of the script name - @type script_type: str - - @return: a new, unique script name - @rtype: str - ''' - - i = 0 - template = script_type + "_%02d" - name = template % (i) - - while True: - - if name in self.scripts: - cmd = self.scripts[name].cmd - if cmd is not None: - if len(cmd): - i += 1 - name = template % (i) - else: - break - else: - break - else: - break - - return name - - #------------------------------------------------------------ - def _start_new_log(self, config_file, rownum): - ''' - Starting a new log definition in self.new_log and filling it - with the current default values. - - @param config_file: the configuration file with the start - of the logfile definition - @type config_file: str - @param rownum: the row number of the configuration file - with the start of the logfile definition - @type rownum: int - ''' - - _ = self.t.lgettext - - if self.verbose > 3: - self.logger.debug( _("Starting a new log directive with default values.")) - - self.new_log = {} - - self.new_log['files'] = [] - self.new_log['file_patterns'] = [] - - self.new_log['compress'] = self.default['compress'] - self.new_log['compresscmd'] = self.default['compresscmd'] - self.new_log['compressext'] = self.default['compressext'] - self.new_log['compressoptions'] = self.default['compressoptions'] - self.new_log['configfile'] = config_file - self.new_log['configrow'] = rownum - self.new_log['copy'] = self.default['copy'] - self.new_log['copytruncate'] = self.default['copytruncate'] - self.new_log['create'] = { - 'enabled': self.default['create']['enabled'], - 'mode': self.default['create']['mode'], - 'owner': self.default['create']['owner'], - 'group': self.default['create']['group'], - } - self.new_log['period'] = self.default['period'] - self.new_log['dateext'] = self.default['dateext'] - self.new_log['datepattern'] = self.default['datepattern'] - self.new_log['delaycompress'] = self.default['delaycompress'] - self.new_log['extension'] = self.default['extension'] - self.new_log['ifempty'] = self.default['ifempty'] - self.new_log['mailaddress'] = self.default['mailaddress'] - self.new_log['mailfirst'] = self.default['mailfirst'] - self.new_log['maxage'] = self.default['maxage'] - self.new_log['missingok'] = self.default['missingok'] - self.new_log['olddir'] = { - 'dirname': self.default['olddir']['dirname'], - 'dateformat': self.default['olddir']['dateformat'], - 'enabled': self.default['olddir']['enabled'], - 'mode': self.default['olddir']['mode'], - 'owner': self.default['olddir']['owner'], - 'group': self.default['olddir']['group'], - } - self.new_log['rotate'] = self.default['rotate'] - self.new_log['sharedscripts'] = self.default['sharedscripts'] - self.new_log['shred'] = self.default['shred'] - self.new_log['size'] = self.default['size'] - self.new_log['start'] = self.default['start'] - - for script_type in script_directives: - self.new_log[script_type] = None - - #------------------------------------------------------------ - def _assign_logfiles(self): - ''' - Finds all existing logfiles of self.new_log according to the - shell matching patterns in self.new_log['file_patterns']. - If a logfile was even defined, a warning is omitted and the - new definition will thrown away. - - @return: number of found logfiles according to self.new_log['file_patterns'] - @rtype: int - ''' - - _ = self.t.lgettext - - if len(self.new_log['file_patterns']) <= 0: - msg = _("No logfile pattern defined.") - self.logger.warning(msg) - return 0 - - for pattern in self.new_log['file_patterns']: - if self.verbose > 1: - msg = _("Find all logfiles for shell matching pattern '%s' ...") \ - % (pattern) - self.logger.debug(msg) - logfiles = glob.glob(pattern) - if len(logfiles) <= 0: - msg = _("No logfile found for pattern '%s'.") % (pattern) - if self.new_log['missingok']: - self.logger.debug(msg) - else: - self.logger.warning(msg) - continue - for logfile in logfiles: - if self.verbose > 1: - msg = _("Found logfile '%(file)s for pattern '%(pattern)s'.") \ - % {'file': logfile, 'pattern': pattern } - self.logger.debug(msg) - if logfile in self.defined_logfiles: - f = self.defined_logfiles[logfile] - msg = ( _("Logfile '%(logfile)s' is even defined (file '%(cfgfile)s', " + - "row %(rownum)d) and so not taken a second time.") - % {'logfile': logfile, - 'cfgfile': f['file'], - 'rownum': f['rownum']} - ) - self.logger.warning(msg) - continue - if self.verbose > 1: - msg = _("Logfile '%s' will taken.") \ - % (logfile) - self.defined_logfiles[logfile] = { - 'file': self.new_log['configfile'], - 'rownum': self.new_log['configrow'], - } - self.new_log['files'].append(logfile) - - return len(self.new_log['files']) - -#======================================================================== - -if __name__ == "__main__": - pass - - -#======================================================================== - -# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotateGetopts.py b/LogRotateGetopts.py deleted file mode 100755 index e74bde7..0000000 --- a/LogRotateGetopts.py +++ /dev/null @@ -1,317 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# $Id$ -# $URL$ - -''' -@author: Frank Brehm -@contact: frank@brehm-online.com -@license: GPL3 -@copyright: (c) 2010-2011 by Frank Brehm, Berlin -@version: 0.0.1 -@summary: Option parser for Python logrotating -''' - -import re -import sys -import gettext - -from optparse import OptionError -from optparse import OptionParser -from optparse import OptionGroup -from optparse import OptionConflictError - - -revision = '$Revision$' -revision = re.sub( r'\$', '', revision ) -revision = re.sub( r'Revision: ', r'r', revision ) -revision = re.sub( r'\s*$', '', revision ) - -__author__ = 'Frank Brehm' -__copyright__ = '(C) 2011 by Frank Brehm, Berlin' -__contact__ = 'frank@brehm-online.com' -__version__ = '0.0.1 ' + revision -__license__ = 'GPL3' - - -#======================================================================== - -class LogrotateOptParserError(Exception): - ''' - Class for exceptions in this module, escpacially - due to false commandline options. - ''' - -#======================================================================== - -class LogrotateOptParser(object): - ''' - Class for parsing commandline options of Python logrotating. - - @author: Frank Brehm - @contact: frank@brehm-online.com - ''' - - #------------------------------------------------------- - def __init__( self, prog = '%prog', - version = None, - local_dir = None, - ): - ''' - Constructor. - - @param prog: The name of the calling process (e.g. sys.argv[0]) - @type prog: str - @param version: The version string to use - @type version: str - @param local_dir: The directory, where the i18n-files (*.mo) - are located. If None, then system default - (/usr/share/locale) is used. - @type local_dir: str or None - - @return: None - ''' - - self.prog = prog - ''' - @ivar: The name of the calling process - @type: str - ''' - - self.version = version - ''' - @ivar: The version string to use - @type: str - ''' - - self.local_dir = local_dir - ''' - @ivar: The directory, where the i18n-files (*.mo) are located. - @type: str or None - ''' - - self.t = gettext.translation( - 'LogRotateGetopts', - local_dir, - fallback = True - ) - ''' - @ivar: a gettext translation object - @type: gettext.translation - ''' - - _ = self.t.lgettext - - self.description = _('Rotates, compresses and mails system logs.') - ''' - @ivar: description of the program - @type: str - ''' - - self.usage = ( _("%s [options] ") + "\n" ) %(prog) - ''' - @ivar: the usage string in getopt help output - @type: str - ''' - self.usage += ( ' %s [-h|-?|--help]\n' %(prog) ) - self.usage += ( ' %s --usage\n' %(prog) ) - self.usage += ( ' %s --version' %(prog) ) - - self.options = None - ''' - @ivar: a dict with all given commandline options - after calling getOpts() - @type: dict or None - ''' - - self.args = None - ''' - @ivar: a list with all commandline parameters, what are not options - @type: list or None - ''' - - self.parsed = False - ''' - @ivar: flag, whether the parsing was done - @type: bool - ''' - - if version: - self.version = version - - self.parser = OptionParser( - prog = self.prog, - version = self.version, - description = self.description, - usage = self.usage, - conflict_handler = "resolve", - ) - ''' - @ivar: the working OptionParser Object - @type: optparse.OptionParser - ''' - - self._add_options() - - #------------------------------------------------------- - def _add_options(self): - ''' - Private function to add all necessary options - to the OptionParser object - ''' - - _ = self.t.ugettext - __ = self.t.ungettext - - if self.parser.has_option('--help'): - self.parser.remove_option('--help') - - if self.parser.has_option('--version'): - self.parser.remove_option('--version') - - self.parser.add_option( - '--simulate', - '--test', - '-T', - default = False, - action = 'store_true', - dest = 'test', - help = _('set this do simulate commands'), - ) - - self.parser.add_option( - '--verbose', - '-v', - default = False, - action = 'count', - dest = 'verbose', - help = _('set the verbosity level'), - ) - - self.parser.add_option( - '--debug', - '-d', - default = False, - action = 'store_true', - dest = 'debug', - help = _("Don't do anything, just test (implies -v and -T)"), - ) - - self.parser.add_option( - '--force', - '-f', - default = False, - action = 'store_true', - dest = 'force', - help = _("Force file rotation"), - ) - - self.parser.add_option( - '--config-check', - '-c', - default = False, - action = 'store_true', - dest = 'configcheck', - help = _("Checks only the given configuration file and does " - + "nothing. Conflicts with -f."), - ) - - self.parser.add_option( - '--state', - '-s', - dest = "statefile", - metavar = 'FILE', - help = _('Path of state file (different to configuration)'), - ) - - self.parser.add_option( - '--pid-file', - '-P', - dest = "pidfile", - metavar = 'FILE', - help = _('Path of PID file (different to configuration)'), - ) - - self.parser.add_option( - '--mail', - '-m', - dest = "mailcmd", - metavar = 'CMD', - help = _('Command to send mail (instead of using ' - + 'the Phyton email package)'), - ) - - ###### - # Option group for common options - - group = OptionGroup(self.parser, _("Common options")) - - group.add_option( - '-h', - '-?', - '--help', - default = False, - action = 'help', - dest = 'help', - help = _('Shows a help message and exit.'), - ) - - group.add_option( - '--usage', - default = False, - action = 'store_true', - dest = 'usage', - help = _('Display brief usage message and exit.'), - ) - - group.add_option( - '-V', - '--version', - default = False, - action = 'version', - dest = 'version', - help = _('Shows the version number of the program and exit.'), - ) - - self.parser.add_option_group(group) - - #---------------------------------------------------------------------- - def getOpts(self): - ''' - Wrapper function to OptionParser.parse_args(). - Sets self.options and self.args with the appropriate values. - @return: None - ''' - - _ = self.t.ugettext - - if not self.parsed: - self.options, self.args = self.parser.parse_args() - self.parsed = True - - if self.options.usage: - self.parser.print_usage() - sys.exit(0) - - if self.options.force and self.options.configcheck: - raise LogrotateOptParserError( _('Invalid usage of --force and ' - + '--config-check.') ) - - if self.args is None or len(self.args) < 1: - raise LogrotateOptParserError( _('No configuration file given.') ) - - if len(self.args) != 1: - raise LogrotateOptParserError( - _('Only one configuration file is allowed.') - ) - -#======================================================================== - -if __name__ == "__main__": - pass - - -#======================================================================== - -# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotateHandler.py b/LogRotateHandler.py deleted file mode 100755 index dda1852..0000000 --- a/LogRotateHandler.py +++ /dev/null @@ -1,2390 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# $Id$ -# $URL$ - -''' -@author: Frank Brehm -@contact: frank@brehm-online.com -@license: GPL3 -@copyright: (c) 2010-2011 by Frank Brehm, Berlin -@version: 0.4.0 -@summary: Application handler module for Python logrotating -''' - -# Für Terminal-Dinge: http://code.activestate.com/recipes/475116/ - -import re -import sys -import gettext -import logging -import pprint -import os -import os.path -import errno -import socket -import subprocess -import shutil -import glob -from datetime import datetime, timedelta -import time -import gzip -import bz2 -import zipfile - -from LogRotateConfig import LogrotateConfigurationError -from LogRotateConfig import LogrotateConfigurationReader - -from LogRotateStatusFile import LogrotateStatusFileError -from LogRotateStatusFile import LogrotateStatusFile -from LogRotateStatusFile import utc - -from LogRotateMailer import LogRotateMailerError -from LogRotateMailer import LogRotateMailer - -revision = '$Revision$' -revision = re.sub( r'\$', '', revision ) -revision = re.sub( r'Revision: ', r'r', revision ) -revision = re.sub( r'\s*$', '', revision ) - -__author__ = 'Frank Brehm' -__copyright__ = '(C) 2011 by Frank Brehm, Berlin' -__contact__ = 'frank@brehm-online.com' -__version__ = '0.4.0 ' + revision -__license__ = 'GPL3' - - -#======================================================================== - -class LogrotateHandlerError(Exception): - ''' - Base class for exceptions in this module. - ''' - -#======================================================================== - -class StdoutFilter(logging.Filter): - ''' - Class, that filters all logrecords - ''' - - def filter(self, record): - ''' - Filtering log records and let through messages - except them with the level names 'WARNING', 'ERROR' or 'CRITICAL'. - - @param record: the record to filter - @type record: logging.LogRecord - - @return: pass the record or not - ''' - if record.levelname == 'WARNING': - return False - if record.levelname == 'ERROR': - return False - if record.levelname == 'CRITICAL': - return False - return True - -#======================================================================== - -class LogrotateHandler(object): - ''' - Class for application handler for Python logrotating - - @author: Frank Brehm - @contact: frank@brehm-online.com - ''' - - #------------------------------------------------------- - def __init__( self, config_file, - test = False, - verbose = 0, - force = False, - config_check = False, - state_file = None, - pid_file = None, - mail_cmd = None, - local_dir = None, - version = None, - ): - ''' - Constructor. - - @param config_file: the configuration file to use - @type config_file: str - @param test: testmode, no real actions are made - @type test: bool - @param verbose: verbosity (debug) level - @type verbose: int - @param force: Force file rotation - @type force: bool - @param config_check: Checks only the configuration and does nothing - @type config_check: bool - @param state_file: Path of state file (different to configuration) - @type state_file: str or None - @param pid_file: Path of PID file (different to configuration) - @type pid_file: str or None - @param mail_cmd: command to send mail (instead of using - the Phyton email package) - @type mail_cmd: str or None - @param local_dir: The directory, where the i18n-files (*.mo) - are located. If None, then system default - (/usr/share/locale) is used. - @type local_dir: str or None - @param version: version number to show - @type version: str - - @return: None - ''' - - self.local_dir = local_dir - ''' - @ivar: The directory, where the i18n-files (*.mo) are located. - @type: str or None - ''' - - self.t = gettext.translation( - 'LogRotateHandler', - local_dir, - fallback = True - ) - ''' - @ivar: a gettext translation object - @type: gettext.translation - ''' - - _ = self.t.lgettext - - self.verbose = verbose - ''' - @ivar: verbosity level (0 - 9) - @type: int - ''' - - self.version = __version__ - ''' - @ivar: version number to show, e.g. as the X-Mailer version - @type: str - ''' - if version is not None: - self.version = version - - self.test = test - ''' - @ivar: testmode, no real actions are made - @type: bool - ''' - - self.force = force - ''' - @ivar: Force file rotation - @type: bool - ''' - - self.state_file = None - ''' - @ivar: the state file object after his initialisation - @type: LogRotateStateFile or None - ''' - - self.state_file_name = state_file - ''' - @ivar: Path of state file (from commandline or from configuration) - @type: str - ''' - - self.pid_file = pid_file - ''' - @ivar: Path of PID file (from commandline or from configuration) - @type: str - ''' - - self.pidfile_created = False - ''' - @ivar: Is a PID file created by this instance and should removed - on destroying this object. - @type: bool - ''' - - self.mail_cmd = mail_cmd - ''' - @ivar: command to send mail (instead of using the Phyton email package) - @type: str or None - ''' - - self.config_file = config_file - ''' - @ivar: the initial configuration file to use - @type: str - ''' - - self.config = [] - ''' - @ivar: the configuration, how it was read from cofiguration file(s) - @type: dict - ''' - - self.scripts = {} - ''' - @ivar: list of LogRotateScript objects with all named scripts found in configuration - @type: list - ''' - - self.template = {} - ''' - @ivar: things to do in olddir stuff - @type: dict - ''' - self._prepare_templates() - - self.logfiles = [] - ''' - @ivar: list of all rotated logfiles. Each entry is a dict with - three keys: - - 'original': str with the name of the unrotated file - - 'rotated': str with the name of the rotated file - - 'oldfiles: list with all old rotated files of this file - - 'desc_index': index of list self.config for appropriate - logfile definition - @type: list - ''' - - self.files_delete = {} - ''' - @ivar: dictionary with all files, they have to delete - @type: dict - ''' - - self.files_compress = {} - ''' - @ivar: dictionary with all files, they have to compress - keys are the filenames, values are the index number - of the list self.config (for compress options) - @type: dict - ''' - - self.files2send = {} - ''' - @ivar: dictionary with all all rotated logfiles to send via - mail to one or more recipients. - Keys are the file names of the (even existing) rotated - and maybe compressed logfiles. - Values are a tuple of (mailaddress, original_logfile), where - mailaddress is a comma separated list of mail addresses of - the recipients of the mails, and original_logfile is the name - of unrotated logfile. - This dict will filled by _do_rotate_file(), and will performed - by send_logfiles(). - @type: dict - ''' - - ################################################# - # Create a logger object - self.logger = logging.getLogger('pylogrotate') - ''' - @ivar: logger object - @type: logging.getLogger - ''' - - self.logger.setLevel(logging.DEBUG) - - # create formatter - format_str = '[%(asctime)s]: %(levelname)-8s - %(message)s' - if test: - format_str = '%(levelname)-8s - %(message)s' - if verbose: - if verbose > 1: - format_str = '[%(asctime)s]: %(name)s %(funcName)s() %(levelname)-8s - %(message)s' - if test: - format_str = '%(name)s %(funcName)s() %(levelname)-8s - %(message)s' - else: - format_str = '[%(asctime)s]: %(name)s %(levelname)-8s - %(message)s' - if test: - format_str = '%(name)s %(levelname)-8s - %(message)s' - formatter = logging.Formatter(format_str) - - # create console handler for error messages - console_stderr = logging.StreamHandler(sys.stderr) - console_stderr.setLevel(logging.WARNING) - console_stderr.setFormatter(formatter) - self.logger.addHandler(console_stderr) - - # create console handler for other messages - console_stdout = logging.StreamHandler(sys.stdout) - if verbose: - console_stdout.setLevel(logging.DEBUG) - else: - console_stdout.setLevel(logging.INFO) - fltr = StdoutFilter() - console_stdout.addFilter(fltr) - console_stdout.setFormatter(formatter) - self.logger.addHandler(console_stdout) - - # define a mailer object - self.mailer = LogRotateMailer( - local_dir = self.local_dir, - verbose = self.verbose, - test_mode = self.test, - mailer_version = self.version, - ) - if mail_cmd: - self.mailer.sendmail = mail_cmd - - # end of init properties - self.logger.debug( _("Logrotating initialised") ) - - if not self.read_configuration(): - self.logger.error( _('Could not read configuration') ) - sys.exit(1) - - if config_check: - return - - if not self._check_pidfile(): - sys.exit(3) - - if not self._write_pidfile(): - sys.exit(3) - - self.logger.debug( _("Logrotating ready for work") ) - - # Create status file object - self.state_file = LogrotateStatusFile( - file_name = self.state_file_name, - local_dir = self.local_dir, - verbose = self.verbose, - test_mode = self.test, - ) - - #------------------------------------------------------------ - def __str__(self): - ''' - Typecasting function for translating object structure - into a string - - @return: structure as string - @rtype: str - ''' - - pp = pprint.PrettyPrinter(indent=4) - structure = self.as_dict() - return pp.pformat(structure) - - #------------------------------------------------------- - def as_dict(self): - ''' - Transforms the elements of the object into a dict - - @return: structure as dict - @rtype: dict - ''' - - res = { - 'config': self.config, - 'config_file': self.config_file, - 'files_delete': self.files_delete, - 'files_compress': self.files_compress, - 'files2send': self.files2send, - 'force': self.force, - 'local_dir': self.local_dir, - 'logfiles': self.logfiles, - 'logger': self.logger, - 'mail_cmd': self.mail_cmd, - 'mailer': self.mailer.as_dict(), - 'scripts': {}, - 'state_file': None, - 'state_file_name': self.state_file_name, - 'pid_file': self.pid_file, - 'pidfile_created': self.pidfile_created, - 't': self.t, - 'test': self.test, - 'template': self.template, - 'verbose': self.verbose, - 'version': self.version, - } - if self.state_file: - res['state_file'] = self.state_file.as_dict() - - for script_name in self.scripts.keys(): - res['scripts'][script_name] = self.scripts[script_name].as_dict() - - return res - - #------------------------------------------------------------ - def __del__(self): - ''' - Destructor. - No parameters, no return value. - ''' - - _ = self.t.lgettext - - if self.pidfile_created: - if os.path.exists(self.pid_file): - self.logger.debug( _("Removing PID file '%s' ...") % (self.pid_file) ) - try: - os.remove(self.pid_file) - except OSError, e: - self.logger.error( _("Error removing PID file '%(file)s': %(msg)") - % { 'file': self.pid_file, 'msg': str(e) } - ) - - #------------------------------------------------------------ - def _prepare_templates(self): - ''' - Preparing self.template with values for placeholders - in olddir stuff. - ''' - - self.template = {} - - hostname = socket.getfqdn() - self.template['nodename'] = hostname - self.template['domain'] = '' - - match = re.search(r'^([^\.]+)\.(.*)', hostname) - if match: - self.template['nodename'] = match.group(1) - self.template['domain'] = match.group(2) - - uname = os.uname() - self.template['sysname'] = uname[0] - self.template['release'] = uname[2] - self.template['version'] = uname[3] - self.template['machine'] = uname[4] - - #------------------------------------------------------------ - def read_configuration(self): - ''' - Reads the configuration from self.config_file - - @return: Success of reading - @rtype: bool - ''' - - _ = self.t.lgettext - - config_reader = LogrotateConfigurationReader( - config_file = self.config_file, - verbose = self.verbose, - local_dir = self.local_dir, - test_mode = self.test, - ) - - if self.verbose > 2: - msg = _("Configuration reader object structure") + ':\n' + str(config_reader) - self.logger.debug(msg) - - try: - self.config = config_reader.get_config() - self.scripts = config_reader.get_scripts() - except LogrotateConfigurationError, e: - self.logger.error( str(e) ) - sys.exit(10) - - if self.verbose > 2: - pp = pprint.PrettyPrinter(indent=4) - msg = _("Found global options:") + "\n" + pp.pformat(config_reader.global_option) - self.logger.debug(msg) - - # Get and set mailer options - if 'mailfrom' in config_reader.global_option and \ - config_reader.global_option['mailfrom']: - self.mailer.from_address = config_reader.global_option['mailfrom'] - if config_reader.global_option['smtphost'] and \ - config_reader.global_option['smtphost'] != 'localhost': - self.mailer.smtp_host = config_reader.global_option['smtphost'] - if 'smtpport' in config_reader.global_option: - self.mailer.smtp_port = config_reader.global_option['smtpport'] - if 'smtptls' in config_reader.global_option: - self.mailer.smtp_tls = config_reader.global_option['smtptls'] - if 'smtpuser' in config_reader.global_option: - self.mailer.smtp_user = config_reader.global_option['smtpuser'] - if 'smtppasswd' in config_reader.global_option: - self.mailer.smtp_passwd = config_reader.global_option['smtppasswd'] - - if self.state_file_name is None: - if 'statusfile' in config_reader.global_option and \ - config_reader.global_option['statusfile'] is not None: - self.state_file_name = config_reader.global_option['statusfile'] - else: - self.state_file_name = os.sep + os.path.join('var', 'lib', 'py-logrotate.status') - self.logger.debug( _("Name of state file: '%s'") % (self.state_file_name) ) - - if self.pid_file is None: - if 'pidfile' in config_reader.global_option and \ - config_reader.global_option['pidfile'] is not None: - self.pid_file = config_reader.global_option['pidfile'] - else: - self.pid_file = os.sep + os.path.join('var', 'run', 'py-logrotate.pid') - self.logger.debug( _("PID file: '%s'") % (self.pid_file) ) - - return True - - #------------------------------------------------------------ - def _check_pidfile(self): - ''' - Checks the existence and consistence of self.pid_file. - - Exit, if there is a running process with a PID from this file. - Doesn't exit in test mode. - - Writes on success (no other process) this PID file. - - @return: Success - @rtype: bool - ''' - - _ = self.t.lgettext - - if not os.path.exists(self.pid_file): - if self.verbose > 1: - self.logger.debug( _("PID file '%s' doesn't exists.") % (self.pid_file) ) - return True - - if self.test: - self.logger.info( _("Testmode, skip test of PID file '%s'.") % (self.pid_file) ) - return True - - self.logger.debug( _("Reading PID file '%s' ...") % (self.pid_file) ) - f = None - try: - f = open(self.pid_file, 'r') - except IOError, e: - raise LogrotateHandlerError( - _("Couldn't open PID file '%(file)s' for reading: %(msg)s") - % { 'file': self.pid_file, 'msg': str(e) } - ) - - line = f.readline() - f.close() - - pid = None - line = line.strip() - match = re.search(r'^\s*(\d+)\s*$', line) - if match: - pid = int(match.group(1)) - else: - self.logger.warn( _("No useful information found in PID file '%(file)s': '%(line)s'") - % { 'file': self.pid_file, 'line': line } - ) - return False - - if self.verbose > 1: - self.logger.debug( _("Trying check for process with PID %d ...") % (pid) ) - try: - os.kill(pid, 0) - except OSError, err: - if err.errno == errno.ESRCH: - self.logger.info( _("Process with PID %d anonymous died.") % (pid) ) - return True - elif err.errno == errno.EPERM: - self.logger.warn( _("No permission to signal the process %d ...") % (pid) ) - return True - else: - self.logger.warn( _("Unknown error: '%s'") % (str(err)) ) - return False - else: - self.logger.error( _("Process with PID %d is allready running.") % (pid) ) - return False - - return False - - #------------------------------------------------------------ - def _write_pidfile(self): - ''' - Writes the PID of the current process in self.pid_file. - - Exit with an error, if it's not possible to write. - Doesn't exit in test mode. - - Writes on success (no other process) this PID file. - - @return: Success - @rtype: bool - ''' - - _ = self.t.lgettext - - if self.test: - self.logger.info( _("Testmode, skip writing of PID file '%s'.") % (self.pid_file) ) - return True - - self.logger.debug( _("Writing PID file '%s' ...") % (self.pid_file) ) - - f = None - try: - f = open(self.pid_file, 'w') - f.write(str(os.getppid()) + "\n") - f.close() - except IOError, e: - raise LogrotateHandlerError( - _("Couldn't open PID file '%(file)s' for writing: %(msg)s") - % { 'file': self.pid_file, 'msg': str(e) } - ) - - self.pidfile_created = True - - return True - - #------------------------------------------------------------ - def rotate(self): - ''' - Starting the underlying rotating. - - @return: None - ''' - - _ = self.t.lgettext - - if len(self.config) < 1: - msg = _("No logfile definitions found.") - self.logger.info(msg) - return - - msg = _("Starting underlying rotation ...") - self.logger.info(msg) - - cur_desc_index = 0 - for d in self.config: - self._rotate_definition(cur_desc_index) - cur_desc_index += 1 - - if self.verbose > 1: - line = 60 * '-' - print line + "\n\n" - - # Check for left over scripts to execute - for scriptname in self.scripts.keys(): - if self.verbose >= 4: - msg = ( _("State of script '%s':") % (scriptname) ) \ - + "\n" + str(self.scripts[scriptname]) - self.logger.debug(msg) - del self.scripts[scriptname] - - return - - #------------------------------------------------------------ - def _rotate_definition(self, cur_desc_index): - ''' - Rotation of a logfile definition from a configuration file. - - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - - @return: None - ''' - - definition = self.config[cur_desc_index] - - _ = self.t.lgettext - - if self.verbose > 1: - line = 60 * '-' - print line + "\n\n" - - if self.verbose >= 4: - pp = pprint.PrettyPrinter(indent=4) - msg = _("Rotating of logfile definition:") + \ - "\n" + pp.pformat(definition) - self.logger.debug(msg) - - # re-reading of status file - self.state_file.read() - - for logfile in definition['files']: - if self.verbose > 1: - line = 30 * '-' - print (line + "\n") - msg = ( _("Performing logfile '%s' ...") % (logfile)) - self.logger.debug(msg) - should_rotate = self._should_rotate(logfile, cur_desc_index) - if self.verbose > 1: - if should_rotate: - msg = _("logfile '%s' WILL rotated.") - else: - msg = _("logfile '%s' will NOT rotated.") - self.logger.debug(msg % (logfile)) - if not should_rotate: - continue - self._rotate_file(logfile, cur_desc_index) - - if self.verbose > 1: - print "\n" - - return - - #------------------------------------------------------------ - def _rotate_file(self, logfile, cur_desc_index): - ''' - Rotates a logfile with all with all necessary actions before - and after rotation. - - Throughs an LogrotateHandlerError on error. - - @param logfile: the logfile to rotate - @type logfile: str - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - - @return: None - ''' - - definition = self.config[cur_desc_index] - - _ = self.t.lgettext - - sharedscripts = definition['sharedscripts'] - firstscript = definition['firstaction'] - prescript = definition['prerotate'] - postscript = definition['postrotate'] - lastscript = definition['lastaction'] - - # Executing of the firstaction script, if it wasn't executed - if firstscript: - if self.verbose > 2: - msg = _("Looking, whether the firstaction script should be executed.") - self.logger.debug(msg) - if not self.scripts[firstscript].done_firstrun: - msg = _("Executing firstaction script '%s' ...") % (firstscript) - self.logger.info(msg) - if not self.scripts[firstscript].execute(): - return - self.scripts[firstscript].done_firstrun = True - - # Executing prerotate scripts, if not sharedscripts or even not executed - if prescript: - if self.verbose > 2: - msg = _("Looking, whether the prerun script should be executed.") - self.logger.debug(msg) - do_it = False - if sharedscripts: - if not self.scripts[prescript].done_prerun: - do_it = True - else: - do_it = True - if do_it: - msg = _("Executing prerun script '%s' ...") % (prescript) - self.logger.info(msg) - if not self.scripts[prescript].execute(): - return - self.scripts[prescript].done_prerun = True - - olddir = self._create_olddir(logfile, cur_desc_index) - if olddir is None: - return - - if not self._do_rotate_file(logfile, cur_desc_index, olddir): - return - - # Looking for postrotate script in a similar way like for the prerotate - if postscript: - if self.verbose > 2: - msg = _("Looking, whether the postrun script should be executed.") - self.logger.debug(msg) - do_it = False - self.scripts[postscript].post_files -= 1 - self.scripts[postscript].do_post = True - if sharedscripts: - if self.scripts[postscript].post_files <= 0: - do_it = True - self.scripts[postscript].do_post = False - else: - do_it = True - if do_it: - msg = _("Executing postrun script '%s' ...") % (postscript) - self.logger.info(msg) - if not self.scripts[postscript].execute(): - return - self.scripts[postscript].done_postrun = True - - # Looking for lastaction script - if lastscript: - if self.verbose > 2: - msg = _("Looking, whether the lastaction script should be executed.") - self.logger.debug(msg) - do_it = False - self.scripts[lastscript].last_files -= 1 - self.scripts[lastscript].do_last = True - if self.scripts[lastscript].done_lastrun: - self.scripts[lastscript].do_last = False - else: - if self.scripts[lastscript].last_files <= 0: - do_it = True - self.scripts[lastscript].do_last = False - if do_it: - msg = _("Executing lastaction script '%s' ...") % (lastscript) - self.logger.info(msg) - if not self.scripts[lastscript].execute(): - return - self.scripts[lastscript].done_lastrun = True - - #------------------------------------------------------------ - def _do_rotate_file(self, logfile, cur_desc_index, olddir = None): - ''' - The underlaying unconditionally rotation of a logfile. - - After the successful rotation - - @param logfile: the logfile to rotate - @type logfile: str - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - @param olddir: the directory of the rotated logfile - if "." or None, store the rotated logfile - in their original directory - @type olddir: str or None - - @return: successful or not - @rtype: bool - ''' - - definition = self.config[cur_desc_index] - - if (olddir is not None) and (olddir == "."): - olddir = None - - _ = self.t.lgettext - - uid = os.geteuid() - gid = os.getegid() - - msg = _("Do rotate logfile '%s' ...") % (logfile) - self.logger.debug(msg) - - target = self._get_rotation_target(logfile, cur_desc_index, olddir) - rotations = self._get_rotations(logfile, target, cur_desc_index) - - extension = rotations['extension'] - compress_extension = rotations['compress_extension'] - - # First move all cyclic stuff - for pair in rotations['move']: - file_from = pair['from'] - file_to = pair['to'] - if pair['compressed']: - file_from += compress_extension - file_to += compress_extension - msg = _("Moving file '%(from)s' => '%(to)s'.") \ - % {'from': file_from, 'to': file_to } - self.logger.info(msg) - if not self.test: - try: - shutil.move(file_from, file_to) - except OSError: - msg = _("Error on moving '%(from)s' => '%(to)s': %(err)s") \ - % {'from': file_from, 'to': file_to, 'err': e.strerror} - self.logger.error(msg) - return False - - # Now the underlaying rotation - file_from = rotations['rotate']['from'] - file_to = rotations['rotate']['to'] - - # First check for an existing mail address - if definition['mailaddress'] and definition['mailfirst']: - self.mailer.send_file(file_from, definition['mailaddress']) - - # separate between copy(truncate) and move (and create) - if definition['copytruncate'] or definition['copy']: - # Copying logfile to target - msg = _("Copying file '%(from)s' => '%(to)s'.") \ - % {'from': file_from, 'to': file_to } - self.logger.info(msg) - if not self.test: - try: - shutil.copy2(file_from, file_to) - except OSError: - msg = _("Error on copying '%(from)s' => '%(to)s': %(err)s") \ - % {'from': file_from, 'to': file_to, 'err': e.strerror} - self.logger.error(msg) - return False - if definition['copytruncate']: - msg = _("Truncating file '%s'.") % (file_from) - self.logger.info(msg) - if not self.test: - try: - fd = open(file_from, 'w') - fd.close() - except IOError, e: - msg = _("Error on truncing file '%(from)s': %(err)s") \ - % {'from': file_from, 'err': str(e)} - self.logger.error(msg) - return False - - else: - - # Moving logfile to target - msg = _("Moving file '%(from)s' => '%(to)s'.") \ - % {'from': file_from, 'to': file_to } - self.logger.info(msg) - - # get old permissions of logfile - statinfo = os.stat(file_from) - - if not self.test: - try: - shutil.move(file_from, file_to) - except OSError: - msg = _("Error on moving '%(from)s' => '%(to)s': %(err)s") \ - % {'from': file_from, 'to': file_to, 'err': e.strerror} - self.logger.error(msg) - return False - - if definition['create']['enabled']: - - # Recreate logfile - msg = _("Recreating file '%s'.") % (file_from) - self.logger.info(msg) - if not self.test: - try: - fd = open(file_from, 'w') - fd.close() - except IOError, e: - msg = _("Error on creating file '%(from)s': %(err)s") \ - % {'from': file_from, 'err': str(e)} - self.logger.error(msg) - return False - - # Setting permissions and ownership - new_mode = statinfo.st_mode - new_uid = statinfo.st_uid - new_gid = statinfo.st_gid - - if not definition['create']['mode'] is None: - new_mode = definition['create']['mode'] - if not definition['create']['owner'] is None: - new_uid = definition['create']['owner'] - if not definition['create']['group'] is None: - new_gid = definition['create']['group'] - - statinfo = os.stat(file_from) - old_mode = statinfo.st_mode - old_uid = statinfo.st_uid - old_gid = statinfo.st_gid - - # Check and set permissions of new logfile - if new_mode != old_mode: - msg = _("Setting permissions of '%(file)s' to %(mode)4o.") \ - % {'file': file_from, 'mode': new_mode} - self.logger.info(msg) - if not self.test: - try: - os.chmod(file_from, new_mode) - except OSError, e: - msg = _("Error on chmod of '%(file)s': %(err)s") \ - % {'file': file_from, 'err': e.strerror} - self.logger.warning(msg) - - # Check and set ownership of new logfile - if (new_uid != old_uid) or (new_gid != old_gid): - myuid = os.geteuid() - if myuid != 0: - msg = _("Only root may execute chown().") - if self.test: - self.logger.info(msg) - else: - self.logger.warning(msg) - else: - msg = _("Setting ownership of '%(file)s' to uid %(uid)d and gid %(gid)d.") \ - % {'file': file_from, 'uid': new_uid, 'gid': new_gid} - self.logger.info(msg) - if not self.test: - try: - os.chown(file_from, new_uid, new_gid) - except OSError, e: - msg = _("Error on chown of '%(file)s': %(err)s") \ - % {'file': file_from, 'err': e.strerror} - self.logger.warning(msg) - - oldfiles = self._collect_old_logfiles(logfile, extension, compress_extension, cur_desc_index) - - # get files to delete and save them back in self.files_delete - files_delete = self._collect_files_delete(oldfiles, cur_desc_index) - if len(files_delete): - for oldfile in files_delete: - self.files_delete[oldfile] = True - if definition['mailaddress'] and not definition['mailfirst']: - self.files2send[oldfile] = (definition['mailaddress'], logfile) - - # get files to compress save them back in self.files_compress - files_compress = self._collect_files_compress(oldfiles, compress_extension, cur_desc_index) - if len(files_compress): - for oldfile in files_compress: - self.files_compress[oldfile] = cur_desc_index - - # write back date of rotation into state file - self.state_file.set_rotation_date(logfile) - self.state_file.write() - - return True - - #------------------------------------------------------------ - def _collect_files_compress(self, oldfiles, compress_extension, cur_desc_index): - ''' - Collects a list with all old logfiles, they have to compress. - - @param oldfiles: a dict whith all found old logfiles as keys and - their modification time as values - @type oldfiles: dict - @param compress_extension: file extension for rotated and - compressed logfiles - @type compress_extension: str - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - - @return: all old (and compressed) logfiles to delete - @rtype: list - ''' - - definition = self.config[cur_desc_index] - _ = self.t.lgettext - - if self.verbose > 2: - msg = _("Retrieving logfiles to compress ...") - self.logger.debug(msg) - - result = [] - - if not definition['compress']: - if self.verbose > 3: - msg = _("No compression defined.") - self.logger.debug(msg) - return result - - if not oldfiles.keys(): - if self.verbose > 3: - msg = _("No old logfiles available.") - self.logger.debug(msg) - return result - - no_compress = definition['delaycompress'] - if no_compress is None: - no_compress = 0 - - ce = re.escape(compress_extension) - for oldfile in sorted(oldfiles.keys(), key=lambda x: oldfiles[x], reverse=True): - - match = re.search(ce + r'$', oldfile) - if match: - if self.verbose > 2: - msg = _("File '%s' seems to be compressed, skip it.") % (oldfile) - self.logger.debug(msg) - continue - - if oldfile in self.files_delete: - if self.verbose > 2: - msg = _("File '%s' will be deleted, compression unnecessary.") % (oldfile) - self.logger.debug(msg) - continue - - if no_compress: - if self.verbose > 2: - msg = _("Compression of file '%s' will be delayed.") % (oldfile) - self.logger.debug(msg) - no_compress -= 1 - continue - - result.append(oldfile) - - if self.verbose > 3: - if len(result): - pp = pprint.PrettyPrinter(indent=4) - msg = _("Found logfiles to compress:") + "\n" + pp.pformat(result) - self.logger.debug(msg) - else: - msg = _("No old logfiles to compress found.") - self.logger.debug(msg) - return result - - #------------------------------------------------------------ - def _collect_files_delete(self, oldfiles, cur_desc_index): - ''' - Collects a list with all old (and compressed) logfiles, they have to delete. - - @param oldfiles: a dict whith all found old logfiles as keys and - their modification time as values - @type oldfiles: dict - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - - @return: all old (and compressed) logfiles to delete - @rtype: list - ''' - - definition = self.config[cur_desc_index] - _ = self.t.lgettext - - if self.verbose > 2: - msg = _("Retrieving logfiles to delete ...") - self.logger.debug(msg) - - result = [] - - if not oldfiles.keys(): - if self.verbose > 3: - msg = _("No old logfiles available.") - self.logger.debug(msg) - return result - - # Maxage in seconds or None - maxage = definition['maxage'] - if maxage is None: - if self.verbose >= 4: - msg = _("No maxage given.") - self.logger.debug(msg) - else: - maxage *= (24 * 60 * 60) - if self.verbose >= 4: - msg = _("Maxage: %d seconds") % (maxage) - self.logger.debug(msg) - - # Number of rotations or Zero - rotate = definition['rotate'] - if rotate is None: - rotate = 0 - if self.verbose >= 4: - msg = _("Max. count rotations: %d") % (rotate) - self.logger.debug(msg) - - count = len(oldfiles.keys()) - for oldfile in sorted(oldfiles.keys(), key=lambda x: oldfiles[x]): - count -= 1 - age = int(time.time() - oldfiles[oldfile]) - if self.verbose > 3: - msg = _("Checking file '%s' for deleting ...") % (oldfile) - self.logger.debug(msg) - if self.verbose >= 4: - msg = _("Current count: %(count)d, current age: %(age)d seconds") \ - % {'count': count, 'age': age} - self.logger.debug(msg) - - # Delete all files, their count is more than the rotate option - if rotate: - if count >= rotate: - if self.verbose >= 3: - msg = _("Deleting '%s' because of too much.") % (oldfile) - self.logger.debug(msg) - result.append(oldfile) - continue - - # Now checking for maximum age - if maxage: - if age >= maxage: - if self.verbose >= 3: - msg = _("Deleting '%s' because of too old.") % (oldfile) - self.logger.debug(msg) - result.append(oldfile) - - if self.verbose > 3: - if len(result): - pp = pprint.PrettyPrinter(indent=4) - msg = _("Found logfiles to delete:") + "\n" + pp.pformat(result) - self.logger.debug(msg) - else: - msg = _("No old logfiles to delete found.") - self.logger.debug(msg) - return result - - #------------------------------------------------------------ - def _collect_old_logfiles(self, logfile, extension, compress_extension, cur_desc_index): - ''' - Collect all rotated versions of this logfile and gives back the - information about. - - @param logfile: the logfile to rotate - @type logfile: str - @param extension: additional fix file extension for rotated logfiles - @type extension: str - @param compress_extension: file extension for rotated and - compressed logfiles - @type compress_extension: str - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - - @return: all found old rotated logfiles as keys - and the last modification timestamp of these files as values - @rtype: dict - ''' - - definition = self.config[cur_desc_index] - _ = self.t.lgettext - - if self.verbose > 2: - msg = _("Retrieving all old logfiles for file '%s' ...") % (logfile) - self.logger.debug(msg) - - result = {} - - basename = os.path.basename(logfile) - dirname = os.path.dirname(logfile) - - if definition['dateext']: - basename += '.*' - - if definition['olddir']['dirname']: - # Create a file pattern depending on olddir definition - - olddir = definition['olddir']['dirname'] - - # Substitution of $dirname - olddir = re.sub(r'(?:\${dirname}|\$dirname(?![a-zA-Z0-9_]))', dirname, olddir) - - # Substitution of $basename - olddir = re.sub(r'(?:\${basename}|\$basename(?![a-zA-Z0-9_]))', basename, olddir) - - # Substitution of $nodename - olddir = re.sub(r'(?:\${nodename}|\$nodename(?![a-zA-Z0-9_]))', self.template['nodename'], olddir) - - # Substitution of $domain - olddir = re.sub(r'(?:\${domain}|\$domain(?![a-zA-Z0-9_]))', self.template['domain'], olddir) - - # Substitution of $machine - olddir = re.sub(r'(?:\${machine}|\$machine(?![a-zA-Z0-9_]))', self.template['machine'], olddir) - - # Substitution of $release - olddir = re.sub(r'(?:\${release}|\$release(?![a-zA-Z0-9_]))', self.template['release'], olddir) - - # Substitution of $sysname - olddir = re.sub(r'(?:\${sysname}|\$sysname(?![a-zA-Z0-9_]))', self.template['sysname'], olddir) - - if not os.path.isabs(olddir): - olddir = os.path.join(dirname, olddir) - olddir = os.path.normpath(olddir) - - #### - # Substituting all datetime.strftime() placeholders by shell pattern - - # weekday - olddir = re.sub(r'%[aA]', '*', olddir) - # name of month - olddir = re.sub(r'%[bBh]', '*', olddir) - # complete date - olddir = re.sub(r'%c', '*', olddir) - # century - olddir = re.sub(r'%C', '[0-9][0-9]', olddir) - # day of month - olddir = re.sub(r'%d', '[0-9][0-9]', olddir) - # date as %m/%d/%y - olddir = re.sub(r'%[Dx]', '[0-9][0-9]/[0-9][0-9]/[0-9][0-9]', olddir) - # Hour in 24-hours format - olddir = re.sub(r'%H', '[012][0-9]', olddir) - # Hour in 12-hours format - olddir = re.sub(r'%J', '[01][0-9]', olddir) - # number of month - olddir = re.sub(r'%m', '[01][0-9]', olddir) - # minute - olddir = re.sub(r'%M', '[0-5][0-9]', olddir) - # AM/PM - olddir = re.sub(r'%p', '[AP]M', olddir) - # complete time in 12-hours format with AM/PM - olddir = re.sub(r'%r', '[01][0-9]:[0-5][0-9]:[0-5][0-9] [AP]M', olddir) - # time in format %H:%M - olddir = re.sub(r'%R', '[012][0-9]:[0-5][0-9]', olddir) - # seconds - olddir = re.sub(r'%S', '[0-5][0-9]', olddir) - # complete time in 24-hours format - olddir = re.sub(r'%[TX]', '[012][0-9]:[0-5][0-9]:[0-5][0-9]', olddir) - # weekday as a number (0-7) - olddir = re.sub(r'%[uw]', '[0-7]', olddir) - # number of week in year (00-53) - olddir = re.sub(r'%[UVW]', '[0-5][0-9]', olddir) - # last two digits of the year - olddir = re.sub(r'%y', '[0-9][0-9]', olddir) - # year complete - olddir = re.sub(r'%Y', '[12][0-9][0-9][0-9]', olddir) - # time zone numeric - olddir = re.sub(r'%z', '[-+][0-9][0-9][0-9][0-9]', olddir) - # time zone name - olddir = re.sub(r'%Z', '*', olddir) - - dirname = olddir - - # composing file pattern - file_pattern = os.path.join(dirname, basename) - pattern_list = [] - pattern_list.append(file_pattern + extension) - pattern_list.append(file_pattern + '.[0-9]' + extension) - pattern_list.append(file_pattern + '.[0-9][0-9]' + extension) - pattern_list.append(file_pattern + '.[0-9][0-9][0-9]' + extension) - pattern_list.append(file_pattern + '.[0-9][0-9][0-9][0-9]' + extension) - pattern_list.append(file_pattern + '.[0-9][0-9][0-9][0-9][0-9]' + extension) - - if definition['compress']: - ext = extension + compress_extension - pattern_list.append(file_pattern + ext) - pattern_list.append(file_pattern + '.[0-9]' + ext) - pattern_list.append(file_pattern + '.[0-9][0-9]' + ext) - pattern_list.append(file_pattern + '.[0-9][0-9][0-9]' + ext) - pattern_list.append(file_pattern + '.[0-9][0-9][0-9][0-9]' + ext) - pattern_list.append(file_pattern + '.[0-9][0-9][0-9][0-9][0-9]' + ext) - - for pattern in pattern_list: - if self.verbose > 2: - msg = _("Search for pattern '%s' ...") % (pattern) - self.logger.debug(msg) - found_files = glob.glob(pattern) - for oldfile in found_files: - oldfile = os.path.abspath(oldfile) - if oldfile == logfile: - continue - statinfo = os.stat(oldfile) - result[oldfile] = statinfo.st_mtime - - if self.verbose > 3: - pp = pprint.PrettyPrinter(indent=4) - msg = _("Found old logfiles:") + "\n" + pp.pformat(result) - self.logger.debug(msg) - return result - - #------------------------------------------------------------ - def _get_rotations(self, logfile, target, cur_desc_index): - ''' - Retrieves all files to move and to rotate and gives them back - as a dict. - - @param logfile: the logfile to rotate - @type logfile: str - @param target: name of the rotated logfile - @type target: str - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - - @return: dict in the form:: - { - 'compress_extension': '.gz', - 'extension': '', - 'rotate': { - 'from': , - 'to': - }, - 'move': [ - ... - { 'from': , 'to': , 'compressed': True}, - { 'from': , 'to': , 'compressed': True}, - { 'from': , 'to': , 'compressed': False}, - ], - } - - the order in the list 'move' is the order, how the - files have to rename. - @rtype: dict - ''' - - definition = self.config[cur_desc_index] - _ = self.t.lgettext - - if self.verbose > 2: - msg = _("Retrieving all movings and rotations for logfile '%(file)s' to target '%(target)s' ...") \ - % {'file': logfile, 'target': target} - self.logger.debug(msg) - - result = { 'rotate': {}, 'move': [] } - - # retrieve additional file extension of logfile after rotation - # without compress extension - extension = definition['extension'] - if extension is None: - extension = '' - match = re.search(r'^\s*$', extension) - if match: - extension = '' - if extension != '': - match = re.search(r'^\.', extension) - if not match: - extension = "." + extension - result['extension'] = extension - extension_wo_compress = extension - - # retrieve additional file extension of logfile after rotation - # for compress extension - compress_extension = '' - if definition['compress']: - compress_extension = definition['compressext'] - match = re.search(r'^\.', compress_extension) - if not match: - compress_extension = "." + compress_extension - result['compress_extension'] = compress_extension - - # appending a trailing '.0', if there are no other differences - # between logfile and target - i = definition['start'] - if i is None: - i = 0 - resulting_target = target + extension_wo_compress - target_wo_number = resulting_target - if resulting_target == logfile: - resulting_target = resulting_target + "." + str(i) - - result['rotate']['from'] = logfile - result['rotate']['to'] = resulting_target - - # resulting target exists, retrieve cyclic rotation - if os.path.exists(resulting_target): - if self.verbose > 3: - msg = _("Resulting target '%s' exists, retrieve cyclic rotation ...") \ - % (resulting_target) - self.logger.debug(msg) - target_wo_cext_old = target_wo_number + "." + str(i) - target_with_cext_old = target_wo_cext_old + compress_extension - while os.path.exists(target_wo_cext_old) or os.path.exists(target_with_cext_old): - i += 1 - target_wo_cext_new = target_wo_number + "." + str(i) - target_with_cext_new = target_wo_cext_new + compress_extension - if self.verbose > 4: - msg = _("Cyclic rotation from '%(from)s' to '%(to)s'.") \ - % {'from': target_wo_cext_old, 'to': target_wo_cext_new} - self.logger.debug(msg) - pair = { - 'from': target_wo_cext_old, - 'to': target_wo_cext_new, - 'compressed': False, - } - if definition['compress']: - if os.path.exists(target_with_cext_old): - pair['compressed'] = True - result['move'].insert(0, pair) - target_wo_cext_old = target_wo_cext_new - target_with_cext_old = target_with_cext_new - - if self.verbose > 3: - pp = pprint.PrettyPrinter(indent=4) - msg = _("Found rotations:") + "\n" + pp.pformat(result) - self.logger.debug(msg) - return result - - #------------------------------------------------------------ - def _get_rotation_target(self, logfile, cur_desc_index, olddir = None): - ''' - Retrieves the name of the rotated logfile and gives it back. - - @param logfile: the logfile to rotate - @type logfile: str - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - @param olddir: the directory of the rotated logfile - if None, store the rotated logfile - in their original directory - @type olddir: str or None - - @return: name of the rotated logfile - @rtype: str - ''' - - definition = self.config[cur_desc_index] - - _ = self.t.lgettext - - if self.verbose > 2: - msg = _("Retrieving the name of the rotated file of '%s' ...") % (logfile) - self.logger.debug(msg) - - target = logfile - if olddir is not None: - basename = os.path.basename(logfile) - target = os.path.join(olddir, basename) - - if definition['dateext']: - pattern = definition['datepattern'] - if pattern is None: - pattern = '%Y-%m-%d' - dateext = datetime.utcnow().strftime(pattern) - if self.verbose > 3: - msg = _("Using date extension '.%(ext)s' from pattern '%(pattern)s'.") \ - % {'ext': dateext, 'pattern': pattern} - self.logger.debug(msg) - target += "." + dateext - - if self.verbose > 1: - msg = _("Using '%(target)s' as target for rotation of logfile '%(logfile)s'.") \ - % {'target': target, 'logfile': logfile} - self.logger.debug(msg) - return target - - #------------------------------------------------------------ - def _create_olddir(self, logfile, cur_desc_index): - ''' - Creating the olddir, if necessary. - - @param logfile: the logfile to rotate - @type logfile: str - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - - @return: Name of the retrieved olddir, ".", if storing - the rotated logfiles in their original directory or - None in case of some minor errors (olddir couldn't - created a.s.o.) - @rtype: str or None - ''' - - definition = self.config[cur_desc_index] - - _ = self.t.lgettext - - uid = os.geteuid() - gid = os.getegid() - - o = definition['olddir'] - if not o['dirname']: - if self.verbose > 1: - msg = _("No dirname directive for olddir given.") - self.logger.debug(msg) - return "." - olddir = o['dirname'] - - mode = o['mode'] - if mode is None: - mode = int('0755', 8) - owner = o['owner'] - if not owner: - owner = uid - group = o['group'] - if not group: - group = gid - - basename = os.path.basename(logfile) - dirname = os.path.dirname(logfile) - - match = re.search(r'%', olddir) - if match: - o['dateformat'] = True - olddir = datetime.utcnow().strftime(olddir) - - # Substitution of $dirname - olddir = re.sub(r'(?:\${dirname}|\$dirname(?![a-zA-Z0-9_]))', dirname, olddir) - - # Substitution of $basename - olddir = re.sub(r'(?:\${basename}|\$basename(?![a-zA-Z0-9_]))', basename, olddir) - - # Substitution of $nodename - olddir = re.sub(r'(?:\${nodename}|\$nodename(?![a-zA-Z0-9_]))', self.template['nodename'], olddir) - - # Substitution of $domain - olddir = re.sub(r'(?:\${domain}|\$domain(?![a-zA-Z0-9_]))', self.template['domain'], olddir) - - # Substitution of $machine - olddir = re.sub(r'(?:\${machine}|\$machine(?![a-zA-Z0-9_]))', self.template['machine'], olddir) - - # Substitution of $release - olddir = re.sub(r'(?:\${release}|\$release(?![a-zA-Z0-9_]))', self.template['release'], olddir) - - # Substitution of $sysname - olddir = re.sub(r'(?:\${sysname}|\$sysname(?![a-zA-Z0-9_]))', self.template['sysname'], olddir) - - if not os.path.isabs(olddir): - olddir = os.path.join(dirname, olddir) - olddir = os.path.normpath(olddir) - - if self.verbose > 1: - msg = _("Olddir name is now '%s'") % (olddir) - self.logger.debug(msg) - - # Check for Existence and Consistence - if os.path.exists(olddir): - if os.path.isdir(olddir): - if os.access(olddir, (os.W_OK | os.X_OK)): - if self.verbose > 2: - msg = _("Olddir '%s' allready exists, not created.") % (olddir) - self.logger.debug(msg) - olddir = os.path.realpath(olddir) - return olddir - else: - msg = _("No write and execute access to olddir '%s'.") % (olddir) - if self.test: - self.logger.warning(msg) - return olddir - raise LogrotateHandlerError(msg) - return None - else: - msg = _("Olddir '%s' exists, but is not a valid directory.") % (olddir) - raise LogrotateHandlerError(msg) - return None - - dirs = [] - dir_head = olddir - while dir_head != os.sep: - (dir_head, dir_tail) = os.path.split(dir_head) - dirs.insert(0, dir_tail) - if self.verbose > 2: - msg = _("Directory chain to create: '%s'") % (str(dirs)) - self.logger.debug(msg) - - # Create olddir recursive, if necessary - msg = _("Creating olddir '%s' recursive ...") % (olddir) - self.logger.info(msg) - create_dir = None - parent_statinfo = os.stat(os.sep) - parent_mode = parent_statinfo.st_mode - parent_uid = parent_statinfo.st_uid - parent_gid = parent_statinfo.st_gid - while len(dirs): - dir_head = dirs.pop(0) - if create_dir: - create_dir = os.path.join(create_dir, dir_head) - else: - create_dir = os.sep + dir_head - if self.verbose > 3: - msg = _("Try to create directory '%s' ...") % (create_dir) - self.logger.debug(msg) - if os.path.exists(create_dir): - if os.path.isdir(create_dir): - if self.verbose > 3: - msg = _("Directory '%s' allready exists, not created.") % (create_dir) - self.logger.debug(msg) - parent_statinfo = os.stat(create_dir) - parent_mode = parent_statinfo.st_mode - parent_uid = parent_statinfo.st_uid - parent_gid = parent_statinfo.st_gid - continue - else: - msg = _("Directory '%s' exists, but is not a valid directory.") % (create_dir) - self.logger.error(msg) - return None - msg = _("Creating directory '%s' ...") % (create_dir) - self.logger.debug(msg) - create_mode = parent_mode - if o['mode'] is not None: - create_mode = o['mode'] - create_uid = parent_uid - if o['owner'] is not None: - create_uid = o['owner'] - create_gid = parent_gid - if o['group'] is not None: - create_gid = o['group'] - if self.verbose > 1: - msg = _("Create permissions: %(mode)4o, Owner-UID: %(uid)d, Group-GID: %(gid)d") \ - % {'mode': create_mode, 'uid': create_uid, 'gid': create_gid} - self.logger.debug(msg) - if not self.test: - if self.verbose > 2: - msg = "os.mkdir('%s', %4o)" % (create_dir, create_mode) - self.logger.debug(msg) - try: - os.mkdir(create_dir, create_mode) - except OSError, e: - msg = _("Error on creating directory '%(dir)s': %(err)s") \ - % {'dir': create_dir, 'err': e.strerror} - self.logger.error(msg) - return None - if (create_uid != uid) or (create_gid != gid): - myuid = os.geteuid() - if myuid != 0: - msg = _("Only root may execute chown().") - if self.test: - self.logger.info(msg) - else: - self.logger.warning(msg) - else: - if self.verbose > 2: - msg = "os.chown('%s', %d, %d)" % (create_dir, create_uid, create_gid) - self.logger.debug(msg) - try: - os.chown(create_dir, create_uid, create_gid) - except OSError, e: - msg = _("Error on chowning directory '%(dir)s': %(err)s") \ - % {'dir': create_dir, 'err': e.strerror} - self.logger.error(msg) - return None - - olddir = os.path.realpath(olddir) - return olddir - - #------------------------------------------------------------ - def _execute_command(self, command, force=False, expected_retcode=0): - ''' - Executes the given command as an OS command in a shell. - - @param command: the command to execute - @type command: str - @param force: force executing command even if self.test == True - @type force: bool - @param expected_retcode: expected returncode of the command - (should be 0) - @type expected_retcode: int - - @return: Success of the comand (shell returncode == 0) - @rtype: bool - ''' - - _ = self.t.lgettext - if self.verbose > 3: - msg = _("Executing command: '%s'") % (command) - self.logger.debug(msg) - if not force: - if self.test: - return True - try: - retcode = subprocess.call(command, shell=True) - if self.verbose > 3: - msg = _("Got returncode: '%s'") % (retcode) - self.logger.debug(msg) - if retcode < 0: - msg = _("Child was terminated by signal %d") % (-retcode) - self.logger.error(msg) - return False - if retcode != expected_retcode: - return False - return True - except OSError, e: - msg = _("Execution failed: %s") % (str(e)) - self.logger.error(msg) - return False - - return False - - #------------------------------------------------------------ - def _should_rotate(self, logfile, cur_desc_index): - ''' - Determines, whether a logfile should rotated dependend on - the informations in the definition. - - Throughs an LogrotateHandlerError on harder errors. - - @param logfile: the logfile to inspect - @type logfile: str - @param cur_desc_index: index of self.config for definition - of logfile from configuration file - @type cur_desc_index: int - - @return: to rotate or not - @rtype: bool - ''' - - definition = self.config[cur_desc_index] - - _ = self.t.lgettext - - if self.verbose > 2: - msg = _("Check, whether logfile '%s' should rotated.") % (logfile) - self.logger.debug(msg) - - if not os.path.exists(logfile): - msg = _("logfile '%s' doesn't exists, not rotated") % (logfile) - if not definition['missingok']: - self.logger.error(msg) - else: - if self.verbose > 1: - self.logger.debug(msg) - return False - - if not os.path.isfile(logfile): - msg = _("logfile '%s' is not a regular file, not rotated") % (logfile) - self.logger.warning(msg) - return False - - filesize = os.path.getsize(logfile) - if self.verbose > 2: - msg = _("Filesize of '%(file)s': %(size)d") % {'file': logfile, 'size': filesize} - self.logger.debug(msg) - - if not filesize: - if not definition['ifempty']: - if self.verbose > 1: - msg = _("Logfile '%s' has a filesize of Zero, not rotated") % (logfile) - self.logger.debug(msg) - return False - - if self.force: - if self.verbose > 1: - msg = _("Rotating of '%s' because of force mode.") % (logfile) - self.logger.debug(msg) - return True - - maxsize = definition['size'] - if maxsize is None: - maxsize = 0 - - last_rotated = self.state_file.get_rotation_date(logfile) - if self.verbose > 2: - msg = _("Date of last rotation: %s") %(last_rotated.isoformat(' ')) - self.logger.debug(msg) - next_rotation = last_rotated + timedelta(days = definition['period']) - if self.verbose > 2: - msg = _("Date of next rotation: %s") %(next_rotation.isoformat(' ')) - self.logger.debug(msg) - - if filesize < maxsize: - if self.verbose > 1: - msg = _("Filesize %(filesize)d is less than %(maxsize)d, rotation not necessary.") \ - % {'filesize': filesize, 'maxsize': maxsize} - self.logger.debug(msg) - return False - - curdate = datetime.utcnow().replace(tzinfo = utc) - if next_rotation > curdate: - if self.verbose > 1: - msg = _("Date of next rotation '%(next)s' is in future, rotation not necessary.") \ - % {'next': next_rotation.isoformat(' ')} - self.logger.debug(msg) - return False - - return True - - #------------------------------------------------------------ - def delete_oldfiles(self): - ''' - Deleting of all logfiles in self.files_delete - - @return: None - ''' - - _ = self.t.lgettext - - msg = _("Deletion of all superfluid logfiles ...") - self.logger.debug(msg) - - if not len(self.files_delete.keys()): - msg = _("No logfiles to delete found.") - self.logger.info(msg) - - for logfile in sorted(self.files_delete.keys(), key=str.lower): - msg = _("Deleting file '%s' ...") % (logfile) - self.logger.info(msg) - if not self.test: - try: - os.remove(logfile) - except OSError, e: - msg = _("Error on removing file '%(file)s': %(err)s") \ - % {'file': logfile, 'err': e.strerror} - self.logger.error(msg) - - return - - #------------------------------------------------------------ - def compress(self): - ''' - Compressing all logfiles in self.files_compress - - @return: None - ''' - - _ = self.t.lgettext - - msg = _("Compression of all uncompressed logfiles ...") - self.logger.debug(msg) - - if not len(self.files_compress.keys()): - msg = _("No logfiles to compress found.") - self.logger.info(msg) - - for logfile in sorted(self.files_compress.keys(), key=str.lower): - - cur_desc_index = self.files_compress[logfile] - definition = self.config[cur_desc_index] - command = definition['compresscmd'] - compress_extension = definition['compressext'] - compress_opts = definition['compressoptions'] - - match = re.search(r'^\.', compress_extension) - if not match: - compress_extension = "." + compress_extension - target = logfile + compress_extension - - # Check existence source logfile - if not os.path.exists(logfile): - msg = _("Source file '%s' for compression doesn't exists.") % (logfile) - raise LogrotateHandlerError(msg) - return - - # Check existence target (compressed file) - if os.path.exists(target): - if os.path.samefile(logfile, target): - msg = _("Source file '%(source)s' and target file '%(target)s' are the same file.") \ - % {'source': logfile, 'target': target} - raise LogrotateHandlerError(msg) - return - msg = _("Target file '%s' for compression allready exists.") %(target) - self.logger.warning(msg) - - # Check for filesize Zero => not compressed - filesize = os.path.getsize(logfile) - if filesize <= 0: - msg = _("File '%s' has a size of 0, skip compressing.") % (logfile) - self.logger.info(msg) - continue - - # Execute compressing ... - msg = _("Compressing file '%(file)s' to '%(target)s' with '%(cmd)s' ...") \ - % {'file': logfile, 'target': target, 'cmd': command} - self.logger.info(msg) - - if command == 'internal_gzip': - self._compress_internal_gzip(logfile, target) - elif command == 'internal_bzip2': - self._compress_internal_bzip2(logfile, target) - elif command == 'internal_zip': - self._compress_internal_zip(logfile, target) - else: - self._compress_external(logfile, target, command, compress_opts) - - return - - #------------------------------------------------------------ - def _compress_external(self, source, target, command, options): - ''' - Compression of the given source file to the target file - with an external command. - - It raises a LogrotateHandlerError on uncoverable errors. - - @param source: the source file to compress - @type source: str - @param target: the filename of the compressed file. - @type target: str - @param command: the OS command to use to compress - @type command: str - @param options: additional options to the compress command - possible placeholders inside the options: - - {}: placeholder for sourcefile - - []: placeholder for targetfile - @type options: str - - @return: success or not - @rtype: bool - ''' - - _ = self.t.lgettext - - if self.verbose > 1: - msg = _("Compressing source '%(source)s' to target'%(target)s' with command '%(cmd)s'.") \ - % {'source': source, 'target': target, 'cmd': command} - self.logger.debug(msg) - - if options is None: - options = '' - - # substituting [] in compressoptions with qouted target file name - match = re.search(r'\[\]', options) - if match: - if self.verbose > 3: - msg = _("Substituting '[]' in compressoptions with '%s'.") % ('"' + target + '"') - self.logger.debug(msg) - options = re.sub(r'\[\]', '"' + target + '"', options) - - # substituting or trailing command with quoted source file name - match = re.search(r'\{\}', options) - if match: - if self.verbose > 3: - msg = _("Substituting '{}' in compressoptions with '%s'.") % ('"' + source + '"') - self.logger.debug(msg) - options = re.sub(r'\{\}', '"' + source + '"', options) - else: - options += ' "' + source + '"' - - if self.verbose > 2: - msg = _("Compress options: '%s'.") % (options) - self.logger.debug(msg) - - cmd = command + ' ' + options - - src_statinfo = os.stat(source) - - if not self._execute_command(cmd): - return False - - if not self.test: - if not os.path.exists(target): - msg = _("Target '%s' of compression doesn't exists after executing compression command.") \ - % (target) - self.logger.error(msg) - return False - - if os.path.exists(source): - - self._copy_file_metadata(source=source, target=target) - - # And last, but not least, delete uncompressed file - if self.verbose > 1: - msg = _("Deleting uncompressed file '%s' ...") % (source) - self.logger.debug(msg) - - if not self.test: - try: - os.remove(source) - except OSError, e: - msg = _("Error removing uncompressed file '%(file)s': %(msg)") \ - % {'file': source, 'msg': str(e) } - self.logger.error(msg) - return False - - else: - - self._copy_file_metadata(target=target, statinfo=src_statinfo) - - return True - #------------------------------------------------------------ - def _copy_file_metadata(self, target, source=None, statinfo=None): - ''' - Copy all metadata (owner, permissions, timestamps a.s.o) from - a source file onto a target file. - The target file must be exists. - Either an existing source file (parameter 'source') or the - statinfo of a former existing file (parameter 'statinfo') must - be given. - - It raises a LogrotateHandlerError on uncoverable errors. - - @param target: filename of an existing target file or directory - @type target: str - @param source: filename of an existing source file or directory - or None, if statinfo was given, - has precedence before a given statinfo - @type source: str or None - @param statinfo: stat object from os.stat() or None, if source was given - @type statinfo: stat-object or None - - @return: success or not - @rtype: bool - ''' - - _ = self.t.lgettext - - if source is None and statinfo is None: - msg = _("Neither 'target' nor 'statinfo' was given on calling _copy_file_metadata().") - raise LogrotateHandlerError(msg) - return False - - if not os.path.exists(target): - msg = _("File or directory '%s' doesn't exists.") % (target) - if self.test: - self.logger.info(msg) - return True - self.logger.error(msg) - return False - - new_statinfo = statinfo - old_statinfo = os.stat(target) - - msg = _("Copying all file metadata to target '%s' ...") % (target) - self.logger.info(msg) - - if source is not None: - - # a source object was given - - if not os.path.exists(source): - msg = _("File or directory '%s' doesn't exists.") % (source) - self.logger.error(msg) - return False - - new_statinfo = os.stat(source) - - # Copying permissions and timestamps from source to target - if self.verbose > 1: - msg = _("Copying permissions and timestamps from source '%(src)s' to target '%(target)s'.") \ - % {'src': source, 'target': target} - self.logger.debug(msg) - if not self.test: - shutil.copystat(source, target) - - else: - - # a source statinfo was given - - atime = new_statinfo.st_atime - mtime = new_statinfo.st_mtime - mode = new_statinfo.st_mode - - # Setting atime and mtime - if self.verbose > 1: - msg = _("Setting atime and mtime of target '%s'.") % (target) - self.logger.debug(msg) - if not self.test: - try: - os.utime(target, (atime, mtime)) - except OSError, e: - msg = _("Error on setting times on target file '%(target)s': %(err)s") \ - % {'target': target, 'err': e.strerror} - self.logger.warning(msg) - return False - - # Setting permissions - old_mode = old_statinfo.st_mode - if mode != old_mode: - if self.verbose > 1: - msg = _("Setting permissions of '%(target)s' to %(mode)4o.") \ - % {'target': target, 'mode': new_mode} - self.logger.info(msg) - if not self.test: - try: - os.chmod(target, mode) - except OSError, e: - msg = _("Error on chmod of '%(target)s': %(err)s") \ - % {'target': target, 'err': e.strerror} - self.logger.warning(msg) - return False - - # Copying ownership from source to target - new_uid = new_statinfo.st_uid - new_gid = new_statinfo.st_gid - old_uid = old_statinfo.st_uid - old_gid = old_statinfo.st_gid - - if (old_uid != new_uid) or (old_gid != new_gid): - if self.verbose > 1: - msg = _("Copying ownership from source to target.") - self.logger.debug(msg) - myuid = os.geteuid() - if myuid != 0: - msg = _("Only root may execute chown().") - if self.test: - self.logger.info(msg) - return True - else: - self.logger.warning(msg) - return False - if not self.test: - try: - os.chown(target, old_uid, old_gid) - except OSError, e: - msg = _("Error on chown of '%(file)s': %(err)s") \ - % {'file': target, 'err': e.strerror} - self.logger.warning(msg) - return False - - return True - - #------------------------------------------------------------ - def _compress_internal_zip(self, source, target): - ''' - Compression of the given source file to the target file - with the Python module zipfile. - - It raises a LogrotateHandlerError on some errors. - - @param source: the source file to compress - @type source: str - @param target: the filename of the compressed file. - @type target: str - - @return: success or not - @rtype: bool - ''' - - _ = self.t.lgettext - - if self.verbose > 1: - msg = _("Compressing source '%(source)s' to target'%(target)s' with module zipfile.") \ - % {'source': source, 'target': target} - self.logger.debug(msg) - - if not self.test: - - # open target for writing - f_out = None - try: - f_out = zipfile.ZipFile( - file=target, - mode='w', - compression=zipfile.ZIP_DEFLATED - ) - except IOError, e: - msg = _("Error on open file '%(file)s' on writing: %(err)s") \ - % {'file': target, 'err': str(e)} - self.logger.error(msg) - return False - - basename = os.path.basename(source) - f_out.write(source, basename) - f_out.close() - - self._copy_file_metadata(source=source, target=target) - - # And last, but not least, delete uncompressed file - if self.verbose > 1: - msg = _("Deleting uncompressed file '%s' ...") % (source) - self.logger.debug(msg) - - if not self.test: - try: - os.remove(source) - except OSError, e: - msg = _("Error removing uncompressed file '%(file)s': %(msg)") \ - % {'file': source, 'msg': str(e) } - self.logger.error(msg) - return False - - return True - - #------------------------------------------------------------ - def _compress_internal_gzip(self, source, target): - ''' - Compression of the given source file to the target file - with the Python module gzip. - As compression level is allways used 9 (highest compression). - - It raises a LogrotateHandlerError on some errors. - - @param source: the source file to compress - @type source: str - @param target: the filename of the compressed file. - @type target: str - - @return: success or not - @rtype: bool - ''' - - _ = self.t.lgettext - - if self.verbose > 1: - msg = _("Compressing source '%(source)s' to target'%(target)s' with module gzip.") \ - % {'source': source, 'target': target} - self.logger.debug(msg) - - if not self.test: - # open source for reading - f_in = None - try: - f_in = open(source, 'rb') - except IOError, e: - msg = _("Error on open file '%(file)s' on reading: %(err)s") \ - % {'file': source, 'err': str(e)} - self.logger.error(msg) - return False - - # open target for writing - f_out = None - try: - f_out = gzip.open(target, 'wb') - except IOError, e: - msg = _("Error on open file '%(file)s' on writing: %(err)s") \ - % {'file': target, 'err': str(e)} - self.logger.error(msg) - f_in.close() - return False - - # compress and write target - f_out.writelines(f_in) - # close both files - f_out.close() - f_in.close() - - self._copy_file_metadata(source=source, target=target) - - # And last, but not least, delete uncompressed file - if self.verbose > 1: - msg = _("Deleting uncompressed file '%s' ...") % (source) - self.logger.debug(msg) - - if not self.test: - try: - os.remove(source) - except OSError, e: - msg = _("Error removing uncompressed file '%(file)s': %(msg)") \ - % {'file': source, 'msg': str(e) } - self.logger.error(msg) - return False - - return True - - #------------------------------------------------------------ - def _compress_internal_bzip2(self, source, target): - ''' - Compression of the given source file to the target file - with the Python module bz2. - As compression level is allways used 9 (highest compression). - - It raises a LogrotateHandlerError on some errors. - - @param source: the source file to compress - @type source: str - @param target: the filename of the compressed file. - @type target: str - - @return: success or not - @rtype: bool - ''' - - _ = self.t.lgettext - - if self.verbose > 1: - msg = _("Compressing source '%(source)s' to target'%(target)s' with module bz2.") \ - % {'source': source, 'target': target} - self.logger.debug(msg) - - if not self.test: - # open source for reading - f_in = None - try: - f_in = open(source, 'rb') - except IOError, e: - msg = _("Error on open file '%(file)s' on reading: %(err)s") \ - % {'file': source, 'err': str(e)} - self.logger.error(msg) - return False - - # open target for writing - f_out = None - try: - f_out = bz2.BZ2File(target, 'w') - except IOError, e: - msg = _("Error on open file '%(file)s' on writing: %(err)s") \ - % {'file': target, 'err': str(e)} - self.logger.error(msg) - f_in.close() - return False - - # compress and write target - f_out.writelines(f_in) - # close both files - f_out.close() - f_in.close() - - self._copy_file_metadata(source=source, target=target) - - # And last, but not least, delete uncompressed file - if self.verbose > 1: - msg = _("Deleting uncompressed file '%s' ...") % (source) - self.logger.debug(msg) - - if not self.test: - try: - os.remove(source) - except OSError, e: - msg = _("Error removing uncompressed file '%(file)s': %(msg)") \ - % {'file': source, 'msg': str(e) } - self.logger.error(msg) - return False - - return True - - - #------------------------------------------------------------ - def send_logfiles(self): - ''' - Sending all mails, they should be sent, to their recipients. - ''' - - _ = self.t.lgettext - - if self.verbose > 1: - pp = pprint.PrettyPrinter(indent=4) - msg = _("Struct files2send:") + "\n" + pp.pformat(self.files2send) - self.logger.debug(msg) - - for filename in self.files2send.keys(): - self.mailer.send_file(filename, self.files2send[filename][0], self.files2send[filename][1]) - - return - -#======================================================================== - -if __name__ == "__main__": - pass - - -#======================================================================== - -# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotateMailer.py b/LogRotateMailer.py deleted file mode 100755 index 9ff2628..0000000 --- a/LogRotateMailer.py +++ /dev/null @@ -1,574 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# $Id$ -# $URL$ - -''' -@author: Frank Brehm -@contact: frank@brehm-online.com -@license: GPL3 -@copyright: (c) 2010-2011 by Frank Brehm, Berlin -@version: 0.0.2 -@summary: module for a logrotate mailer object to send - rotated logfiles per mail to a reciepient -''' - -import re -import logging -import pprint -import gettext -import os -import os.path -import sys -import pwd -import socket -import csv - -from datetime import datetime - -import mimetypes -import email.utils -from email import encoders -from email.message import Message -from email.mime.base import MIMEBase -from email.mime.multipart import MIMEMultipart -from email.mime.nonmultipart import MIMENonMultipart -from email.mime.text import MIMEText - -from quopri import encodestring as _encodestring - -from LogRotateCommon import email_valid - -revision = '$Revision$' -revision = re.sub( r'\$', '', revision ) -revision = re.sub( r'Revision: ', r'r', revision ) -revision = re.sub( r'\s*$', '', revision ) - -__author__ = 'Frank Brehm' -__copyright__ = '(C) 2011 by Frank Brehm, Berlin' -__contact__ = 'frank@brehm-online.com' -__version__ = '0.1.0 ' + revision -__license__ = 'GPL3' - -#======================================================================== - -class LogRotateMailerError(Exception): - ''' - Base class for exceptions in this module. - ''' - -#======================================================================== - -class LogRotateMailer(object): - ''' - Class for a mailer object to send - rotated logfiles per mail to a reciepient - - @author: Frank Brehm - @contact: frank@brehm-online.com - ''' - - #------------------------------------------------------- - def __init__( self, local_dir = None, - verbose = 0, - test_mode = False, - mailer_version = None, - ): - ''' - Constructor. - - @param local_dir: The directory, where the i18n-files (*.mo) - are located. If None, then system default - (/usr/share/locale) is used. - @type local_dir: str or None - @param verbose: verbosity (debug) level - @type verbose: int - @param test_mode: test mode - no write actions are made - @type test_mode: bool - @param mailer_version: version of the X-Mailer tag in the mail header - @type mailer_version: str - - @return: None - ''' - - self.t = gettext.translation( - 'LogRotateMailer', - local_dir, - fallback = True - ) - ''' - @ivar: a gettext translation object - @type: gettext.translation - ''' - - _ = self.t.lgettext - - self.verbose = verbose - ''' - @ivar: verbosity level (0 - 9) - @type: int - ''' - - self.test_mode = test_mode - ''' - @ivar: test mode - no write actions are made - @type: bool - ''' - - self.logger = logging.getLogger('pylogrotate.mailer') - ''' - @ivar: logger object - @type: logging.getLogger - ''' - - self._sendmail = None - ''' - @ivar: file name of the sendmail executable - ('/usr/sbin/sendmail' or '/usr/lib/sendmail') - used for sending the mails. - if None, the mails will sended via SMTP - @type: str or None - ''' - self._init_sendmail() - - self._from_address = ('me', 'info@uhu-banane.de') - ''' - @ivar: Mailaddress of the sender, tuple with the real name of - the sender and his mail address as the second value - @type: tuple - ''' - self._init_from_address() - - self._smtp_host = 'localhost' - ''' - @ivar: the hostname to use for SMTP (smarthost), if no - sendmail binary was found - @type: str - ''' - - self._smtp_port = 25 - ''' - @ivar: the port to use for SMTP to the smarthost - @type: int - ''' - - self._smtp_tls = False - ''' - @ivar: use TLS for sending via SMTP to smarthost - @type: bool - ''' - - self.smtp_user = None - ''' - @ivar: Authentication username for SMTP - @type: str or None - ''' - - self.smtp_passwd = None - ''' - @ivar: Authentication password for SMTP - @type: str or None - ''' - - self.mailer_version = __version__ - ''' - @ivar: version of the X-Mailer tag in the mail header - @type: str - ''' - if mailer_version is not None: - self.mailer_version = mailer_version - - #------------------------------------------------------------ - # Defintion of some properties - - #------------------------------------------------------------ - # Property 'from' - def _get_from_address(self): - ''' - Getter method for property 'from_address' - ''' - return email.utils.formataddr(self._from_address) - - def _set_from_address(self, value): - ''' - Setter method for property 'from_address' - ''' - _ = self.t.lgettext - if value is None: - msg = _("The 'From' address may not set to None.") - raise LogRotateMailerError(msg) - pair = ('', '') - if isinstance(value, tuple): - if len(value) < 2: - pair = email.utils.parseaddr(value[0]) - else: - pair = (value[0], value[1]) - else: - pair = email.utils.parseaddr(value) - - if ( (pair[0] is None or pair[0] == '') and - (pair[1] is None or pair[1] == '') ): - msg = _("Invalid mail address given: '%s'.") % (str(value)) - raise LogRotateMailerError(msg) - - if not email_valid(pair[1]): - msg = _("Invalid mail address given: '%s'.") % (str(value)) - raise LogRotateMailerError(msg) - - self._from_address = pair - if self.verbose > 3: - addr = email.utils.formataddr(pair) - msg = _("Set sender mail address to: '%s'.") % (addr) - self.logger.debug(msg) - - def _del_from_address(self): - ''' - Deleter method for property 'from_address' - ''' - self._init_from_address() - - from_address = property(_get_from_address, _set_from_address, _del_from_address, "The mail address of the sender") - - #------------------------------------------------------------ - # Property 'sendmail' - def _get_sendmail(self): - ''' - Getter method for property 'sendmail' - ''' - return self._sendmail - - def _set_sendmail(self, value): - ''' - Setter method for property 'sendmail' - ''' - _ = self.t.lgettext - if value is None or value == '': - self._sendmail = None - return - - if os.path.isabs(value): - if os.path.exists(value): - cmd = os.path.normpath(value) - if os.access(cmd, os.X_OK): - msg = _("Using '%s' as the sendmail command.") % (cmd) - self.logger.debug(msg) - self._sendmail = cmd - return - else: - msg = _("No execute permissions to '%s'.") % (cmd) - self.logger.warning(msg) - return - else: - msg = _("Sendmail command '%s' not found.") % (value) - self.logger.warning(msg) - return - else: - msg = _("Only absolute path allowed for a sendmail command: '%s'.") % (value) - self.logger.warning(msg) - return - - def _del_sendmail(self): - ''' - Deleter method for property 'from_address' - ''' - self._sendmail = None - - sendmail = property(_get_sendmail, _set_sendmail, _del_sendmail, "The sendmail executable for sending mails local") - - #------------------------------------------------------------ - # Property 'smtp_host' - def _get_smtp_host(self): - ''' - Getter method for property 'smtp_host' - ''' - return self._smtp_host - - def _set_smtp_host(self, value): - ''' - Setter method for property 'smtp_host' - ''' - _ = self.t.lgettext - if value: - self._smtp_host = value - - smtp_host = property(_get_smtp_host, _set_smtp_host, None, "The hostname to use for sending mails via SMTP (smarthost)") - - #------------------------------------------------------------ - # Property 'smtp_port' - def _get_smtp_port(self): - ''' - Getter method for property 'smtp_port' - ''' - return self._smtp_port - - def _set_smtp_port(self, value): - ''' - Setter method for property 'smtp_port' - ''' - _ = self.t.lgettext - if value: - port = 25 - try: - port = int(value) - except ValueError, e: - return - if port < 1 or port >= 2**15: - return - self._smtp_port = port - - smtp_port = property(_get_smtp_port, _set_smtp_port, None, "The port to use for sending mails via SMTP") - - #------------------------------------------------------------ - # Property 'smtp_tls' - def _get_smtp_tls(self): - ''' - Getter method for property 'smtp_tls' - ''' - return self._smtp_tls - - def _set_smtp_tls(self, value): - ''' - Setter method for property 'smtp_tls' - ''' - self._smtp_tls = bool(value) - - smtp_tls = property(_get_smtp_tls, _set_smtp_tls, None, "Use TLS for sending mails via SMTP (smarthost)") - - #------------------------------------------------------------ - # Other Methods - - #------------------------------------------------------- - def __del__(self): - ''' - Destructor. - ''' - - _ = self.t.lgettext - if self.verbose > 2: - msg = _("Mailer object will destroyed.") - self.logger.debug(msg) - - #------------------------------------------------------------ - def __str__(self): - ''' - Typecasting function for translating object structure - into a string - - @return: structure as string - @rtype: str - ''' - - pp = pprint.PrettyPrinter(indent=4) - structure = self.as_dict() - return pp.pformat(structure) - - #------------------------------------------------------- - def as_dict(self): - ''' - Transforms the elements of the object into a dict - - @return: structure as dict - @rtype: dict - ''' - - res = {} - res['t'] = self.t - res['verbose'] = self.verbose - res['test_mode'] = self.test_mode - res['logger'] = self.logger - res['sendmail'] = self.sendmail - res['from'] = self.from_address - res['smtp_host'] = self.smtp_host - res['smtp_port'] = self.smtp_port - res['smtp_tls'] = self.smtp_tls - res['smtp_user'] = self.smtp_user - res['smtp_passwd'] = self.smtp_passwd - res['mailer_version'] = self.mailer_version - - return res - - #------------------------------------------------------- - def _init_from_address(self): - ''' - Initialises the sender mail address - ''' - - _ = self.t.lgettext - - cur_user = pwd.getpwuid(os.getuid())[0] - cur_host = socket.getfqdn() - addr = cur_user + '@' + cur_host - - if self.verbose > 3: - msg = _("Using <%s> as the sender mail address.") % (addr) - self.logger.debug(msg) - - self._from_address = (None, addr) - - #------------------------------------------------------- - def _init_sendmail(self): - ''' - Initialises the sendmail with - ''' - - _ = self.t.lgettext - - progs = [ - os.sep + os.path.join('usr', 'sbin', 'sendmail'), - os.sep + os.path.join('usr', 'lib', 'sendmail'), - ] - - if self.verbose > 3: - msg = _("Initial search for the sendmail executable ...") - self.logger.debug(msg) - - for prog in progs: - - if self.verbose > 3: - msg = _("Testing for '%s' ...") % (prog) - self.logger.debug(msg) - - if os.path.exists(prog): - if os.access(prog, os.X_OK): - if self.verbose > 1: - msg = _("Using '%s' as the sendmail command.") % (prog) - self.logger.debug(msg) - self._sendmail = prog - break - else: - msg = _("No execute permissions to '%s'.") % (prog) - self.logger.warning(msg) - - return - - #------------------------------------------------------- - def send_file(self, - filename, - addresses, - original=None, - mime_type='text/plain', - rotate_date=None, - charset=None - ): - ''' - Mails the file with the given file name as an attachement - to the given recipient(s). - - Raises a LogRotateMailerError on harder errors. - - @param filename: The file name of the file to send (the existing, - rotated and maybe compressed logfile). - @type filename: str - @param addresses: A list of tuples of a pair in the form - of the return value of email.utils.parseaddr() - @type addresses: list - @param original: The file name of the original (unrotated) logfile for - informational purposes. - If not given, filename is used instead. - @type original: str or None - @param mime_type: MIME type (content type) of the original logfile, - defaults to 'text/plain' - @type mime_type: str - @param rotate_date: datetime object of rotation, defaults to now() - @type rotate_date: datetime or None - @param charset: character set of (uncompreesed) logfile, if the - mime_type is 'text/plain', defaults to 'utf-8' - @type charset: str or None - - @return: success of sending - @rtype: bool - ''' - - _ = self.t.lgettext - - if not os.path.exists(filename): - msg = _("File '%s' dosn't exists.") % (filename) - self.logger.error(msg) - return False - - if not os.path.isfile(filename): - msg = _("File '%s' is not a regular file.") % (filename) - self.logger.warning(msg) - return False - - basename = os.path.basename(filename) - if not original: - original = os.path.abspath(filename) - - if not rotate_date: - rotate_date = datetime.now() - - msg = _("Sending mail with attached file '%(file)s' to: %(rcpt)s") \ - % {'file': basename, - 'rcpt': ', '.join(map(lambda x: '"' + email.utils.formataddr(x) + '"', addresses))} - self.logger.debug(msg) - - mail_container = MIMEMultipart() - mail_container['Subject'] = ( "Rotated logfile '%s'" % (filename) ) - mail_container['X-Mailer'] = ( "pylogrotate version %s" % (self.mailer_version) ) - mail_container['From'] = self.from_address - mail_container['To'] = ', '.join(map(lambda x: email.utils.formataddr(x), addresses)) - mail_container.preamble = 'You will not see this in a MIME-aware mail reader.\n' - - # Generate Text of the first part of mail body - mailtext = "Rotated Logfile:\n\n" - mailtext += "\t - " + filename + "\n" - mailtext += "\t (" + original + ")\n" - mailtext += "\n" - mailtext += "Date of rotation: " + rotate_date.isoformat(' ') - mailtext += "\n" - mailtext = _encodestring(mailtext, quotetabs=False) - mail_part = MIMENonMultipart('text', 'plain', charset=sys.getdefaultencoding()) - mail_part.set_payload(mailtext) - mail_part['Content-Transfer-Encoding'] = 'quoted-printable' - mail_container.attach(mail_part) - - ctype, encoding = mimetypes.guess_type(filename) - if self.verbose > 3: - msg = _("Guessed content-type: '%(ctype)s' and encoding '%(encoding)s'.") \ - % {'ctype': ctype, 'encoding': encoding } - self.logger.debug(msg) - - if encoding: - if encoding == 'gzip': - ctype = 'application/x-gzip' - elif encoding == 'bzip2': - ctype = 'application/x-bzip2' - else: - ctype = 'application/octet-stream' - - if not ctype: - ctype = mime_type - - maintype, subtype = ctype.split('/', 1) - fp = open(filename, 'rb') - mail_part = MIMEBase(maintype, subtype) - mail_part.set_payload(fp.read()) - fp.close() - if maintype == 'text': - msgtext = mail_part.get_payload() - msgtext = _encodestring(msgtext, quotetabs=False) - mail_part.set_payload(msgtext) - mail_part['Content-Transfer-Encoding'] = 'quoted-printable' - else: - encoders.encode_base64(mail_part) - mail_part.add_header('Content-Disposition', 'attachment', filename=basename) - mail_container.attach(mail_part) - - composed = mail_container.as_string() - if self.verbose > 4: - msg = _("Generated E-mail:") + "\n" + composed - self.logger.debug(msg) - - return True - -#======================================================================== - -if __name__ == "__main__": - pass - - -#======================================================================== - -# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotateScript.py b/LogRotateScript.py deleted file mode 100755 index 34268ca..0000000 --- a/LogRotateScript.py +++ /dev/null @@ -1,550 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# $Id$ -# $URL$ - -''' -@author: Frank Brehm -@contact: frank@brehm-online.com -@license: GPL3 -@copyright: (c) 2010-2011 by Frank Brehm, Berlin -@version: 0.0.2 -@summary: module for a logrotate script object - (for pre- and postrotate actions) -''' -import re -import logging -import subprocess -import pprint -import gettext - -revision = '$Revision$' -revision = re.sub( r'\$', '', revision ) -revision = re.sub( r'Revision: ', r'r', revision ) -revision = re.sub( r'\s*$', '', revision ) - -__author__ = 'Frank Brehm' -__copyright__ = '(C) 2011 by Frank Brehm, Berlin' -__contact__ = 'frank@brehm-online.com' -__version__ = '0.1.0 ' + revision -__license__ = 'GPL3' - -#======================================================================== - -class LogRotateScriptError(Exception): - ''' - Base class for exceptions in this module. - ''' - -#======================================================================== - -class LogRotateScript(object): - ''' - Class for encapsulating a logrotate script - (for pre- and postrotate actions) - - @author: Frank Brehm - @contact: frank@brehm-online.com - ''' - - #------------------------------------------------------- - def __init__( self, name, - local_dir = None, - verbose = 0, - test_mode = False, - ): - ''' - Constructor. - - @param name: the name of the script as an identifier - @type name: str - @param local_dir: The directory, where the i18n-files (*.mo) - are located. If None, then system default - (/usr/share/locale) is used. - @type local_dir: str or None - @param verbose: verbosity (debug) level - @type verbose: int - @param test_mode: test mode - no write actions are made - @type test_mode: bool - - @return: None - ''' - - self.t = gettext.translation( - 'LogRotateScript', - local_dir, - fallback = True - ) - ''' - @ivar: a gettext translation object - @type: gettext.translation - ''' - - _ = self.t.lgettext - - self.verbose = verbose - ''' - @ivar: verbosity level (0 - 9) - @type: int - ''' - - self._name = name - ''' - @ivar: the name of the script as an identifier - @type: str - ''' - - self.test_mode = test_mode - ''' - @ivar: test mode - no write actions are made - @type: bool - ''' - - self.logger = logging.getLogger('pylogrotate.script') - ''' - @ivar: logger object - @type: logging.getLogger - ''' - - self._cmd = [] - ''' - @ivar: List of commands to execute - @type: list - ''' - - self._post_files = 0 - ''' - @ivar: Number of logfiles referencing to this script - as a postrotate script - @type: int - ''' - - self._last_files = 0 - ''' - @ivar: Number of logfiles referencing to this script - as a lastaction script - @type: int - ''' - - self._done_firstrun = False - ''' - @ivar: Flag, whether the script was executed as - a firstaction script - @type: bool - ''' - - self._done_prerun = False - ''' - @ivar: Flag, whether the script was executed as - a prerun script - @type: bool - ''' - - self._done_postrun = False - ''' - @ivar: Flag, whether the script was executed as - a postrun script - @type: bool - ''' - - self._done_lastrun = False - ''' - @ivar: Flag, whether the script was executed as - a lastaction script - @type: bool - ''' - - self._do_post = False - ''' - Runtime flag, that the script should be executed - as an postrun script - ''' - - self._do_last = False - ''' - Runtime flag, that the script should be executed - as an lastaction script - ''' - - #------------------------------------------------------------ - # Defintion of some properties - - #------------------------------------------------------------ - # Property 'name' - def _get_name(self): - ''' - Getter method for property 'name' - ''' - return self._name - - name = property(_get_name, None, None, "Name of the script as an identifier") - - #------------------------------------------------------------ - # Property 'cmd' - def _get_cmd(self): - ''' - Getter method for property 'cmd' - ''' - if len(self._cmd): - return "\n".join(self._cmd) - else: - return None - - def _set_cmd(self, value): - ''' - Setter method for property 'cmd' - ''' - if value: - if isinstance(value, list): - self._cmd = value[:] - else: - self._cmd = [value] - else: - self._cmd = [] - - def _del_cmd(self): - ''' - Deleter method for property 'cmd' - ''' - self._cmd = [] - - cmd = property(_get_cmd, _set_cmd, _del_cmd, "the commands to execute") - - #------------------------------------------------------------ - # Property 'post_files' - def _get_post_files(self): - ''' - Getter method for property 'post_files' - ''' - return self._post_files - - def _set_post_files(self, value): - ''' - Setter method for property 'post_files' - ''' - _ = self.t.lgettext - if isinstance(value, int): - self._post_files = value - else: - msg = _("Invalid value for property '%s' given.") % ('post_files') - raise LogRotateScriptError(msg) - - post_files = property( - _get_post_files, - _set_post_files, - None, - "Number of logfiles referencing to this script as a postrotate script." - ) - - #------------------------------------------------------------ - # Property 'last_files' - def _get_last_files(self): - ''' - Getter method for property 'last_files' - ''' - return self._last_files - - def _set_last_files(self, value): - ''' - Setter method for property 'last_files' - ''' - _ = self.t.lgettext - if isinstance(value, int): - self._last_files = value - else: - msg = _("Invalid value for property '%s' given.") % ('last_files') - raise LogRotateScriptError(msg) - - last_files = property( - _get_last_files, - _set_last_files, - None, - "Number of logfiles referencing to this script as a lastaction script." - ) - - #------------------------------------------------------------ - # Property 'done_firstrun' - def _get_done_firstrun(self): - ''' - Getter method for property 'done_firstrun' - ''' - return self._done_firstrun - - def _set_done_firstrun(self, value): - ''' - Setter method for property 'done_firstrun' - ''' - self._done_firstrun = bool(value) - - done_firstrun = property( - _get_done_firstrun, - _set_done_firstrun, - None, - "Flag, whether the script was executed as a firstaction script." - ) - - #------------------------------------------------------------ - # Property 'done_prerun' - def _get_done_prerun(self): - ''' - Getter method for property 'done_prerun' - ''' - return self._done_prerun - - def _set_done_prerun(self, value): - ''' - Setter method for property 'done_prerun' - ''' - self._done_prerun = bool(value) - - done_prerun = property( - _get_done_prerun, - _set_done_prerun, - None, - "Flag, whether the script was executed as a prerun script." - ) - - #------------------------------------------------------------ - # Property 'done_postrun' - def _get_done_postrun(self): - ''' - Getter method for property 'done_postrun' - ''' - return self._done_postrun - - def _set_done_postrun(self, value): - ''' - Setter method for property 'done_postrun' - ''' - self._done_postrun = bool(value) - - done_postrun = property( - _get_done_postrun, - _set_done_postrun, - None, - "Flag, whether the script was executed as a postrun script." - ) - - #------------------------------------------------------------ - # Property 'done_lastrun' - def _get_done_lastrun(self): - ''' - Getter method for property 'done_lastrun' - ''' - return self._done_lastrun - - def _set_done_lastrun(self, value): - ''' - Setter method for property 'done_lastrun' - ''' - self._done_lastrun = bool(value) - - done_lastrun = property( - _get_done_lastrun, - _set_done_lastrun, - None, - "Flag, whether the script was executed as a lastaction script." - ) - - #------------------------------------------------------------ - # Property 'do_post' - def _get_do_post(self): - ''' - Getter method for property 'do_post' - ''' - return self._do_post - - def _set_do_post(self, value): - ''' - Setter method for property 'do_post' - ''' - self._do_post = bool(value) - - do_post = property( - _get_do_post, - _set_do_post, - None, - "Flag, whether the script should be executed as a postrun script." - ) - - #------------------------------------------------------------ - # Property 'do_last' - def _get_do_last(self): - ''' - Getter method for property 'do_last' - ''' - return self._do_last - - def _set_do_last(self, value): - ''' - Setter method for property 'do_last' - ''' - self._do_last = bool(value) - - do_last = property( - _get_do_last, - _set_do_last, - None, - "Flag, whether the script should be executed as a lastaction script." - ) - - #------------------------------------------------------------ - # Other Methods - - #------------------------------------------------------- - def __del__(self): - ''' - Destructor. - Checks, whether the script should even be run as - a postrun or a lastaction script - ''' - - _ = self.t.lgettext - if self.verbose > 2: - msg = _("Logrotate script object '%s' will destroyed.") % (self.name) - self.logger.debug(msg) - - self.check_for_execute() - - #------------------------------------------------------------ - def __str__(self): - ''' - Typecasting function for translating object structure - into a string - - @return: structure as string - @rtype: str - ''' - - pp = pprint.PrettyPrinter(indent=4) - structure = self.as_dict() - return pp.pformat(structure) - - #------------------------------------------------------- - def as_dict(self): - ''' - Transforms the elements of the object into a dict - - @return: structure as dict - @rtype: dict - ''' - - res = {} - res['t'] = self.t - res['verbose'] = self.verbose - res['name'] = self.name - res['test_mode'] = self.test_mode - res['logger'] = self.logger - res['cmd'] = self._cmd[:] - res['post_files'] = self.post_files - res['last_files'] = self.last_files - res['done_firstrun'] = self.done_firstrun - res['done_prerun'] = self.done_prerun - res['done_postrun'] = self.done_postrun - res['done_lastrun'] = self.done_lastrun - res['do_post'] = self.do_post - res['do_last'] = self.do_last - - return res - - #------------------------------------------------------------ - def add_cmd(self, cmd): - ''' - Adding a command to the list self._cmd - - @param cmd: the command to add to self._cmd - @type cmd: str - - @return: None - ''' - self._cmd.append(cmd) - - #------------------------------------------------------------ - def execute(self, force=False, expected_retcode=0): - ''' - Executes the command as an OS command in a shell. - - @param force: force executing command even - if self.test_mode == True - @type force: bool - @param expected_retcode: expected returncode of the command - (should be 0) - @type expected_retcode: int - - @return: Success of the comand (shell returncode == 0) - @rtype: bool - ''' - - _ = self.t.lgettext - cmd = self.cmd - if cmd is None: - msg = _("No command to execute defined in script '%s'.") % (self.name) - raise LogRotateScriptError(msg) - return False - if self.verbose > 3: - msg = _("Executing script '%(name)s' with command: '%(cmd)s'") \ - % {'name': self.name, 'cmd': cmd} - self.logger.debug(msg) - if not force: - if self.test_mode: - return True - try: - retcode = subprocess.call(command, shell=True) - if self.verbose > 3: - msg = _("Got returncode for script '%(name)s': '%(retcode)s'") \ - % {'name': self.name, 'retcode': retcode} - self.logger.debug(msg) - if retcode < 0: - msg = _("Child in script '%(name)s' was terminated by signal %(retcode)d") \ - % {'name': self.name, 'retcode': -retcode} - self.logger.error(msg) - return False - if retcode != expected_retcode: - return False - return True - except OSError, e: - msg = _("Execution of script '%(name)s' failed: %(error)s") \ - % {'name': self.name, 'error': str(e)} - self.logger.error(msg) - return False - - return False - - #------------------------------------------------------------ - def check_for_execute(self, force=False, expected_retcode=0): - ''' - Checks, whether the script should executed. - - @param force: force executing command even - if self.test_mode == True - @type force: bool - @param expected_retcode: expected returncode of the command - (should be 0) - @type expected_retcode: int - - @return: Success of execution - @rtype: bool - ''' - - _ = self.t.lgettext - msg = _("Checking, whether the script '%s' should be executed.") % (self.name) - self.logger.debug(msg) - - if self.do_post or self.do_last: - result = self.execute(force=force, expected_retcode=expected_retcode) - self.do_post = False - self.do_last = False - return result - - return True - -#======================================================================== - -if __name__ == "__main__": - pass - - -#======================================================================== - -# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/LogRotateStatusFile.py b/LogRotateStatusFile.py deleted file mode 100755 index 5dc4b3c..0000000 --- a/LogRotateStatusFile.py +++ /dev/null @@ -1,575 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# $Id$ -# $URL$ - -''' -@author: Frank Brehm -@contact: frank@brehm-online.com -@license: GPL3 -@copyright: (c) 2010-2011 by Frank Brehm, Berlin -@version: 0.0.2 -@summary: module for operations with the logrotate state file -''' - -import re -import sys -import os -import os.path -import gettext -import logging -import pprint - -from datetime import tzinfo, timedelta, datetime, date, time - -from LogRotateCommon import split_parts - -revision = '$Revision$' -revision = re.sub( r'\$', '', revision ) -revision = re.sub( r'Revision: ', r'r', revision ) -revision = re.sub( r'\s*$', '', revision ) - -__author__ = 'Frank Brehm' -__copyright__ = '(C) 2011 by Frank Brehm, Berlin' -__contact__ = 'frank@brehm-online.com' -__version__ = '0.1.0 ' + revision -__license__ = 'GPL3' - -#======================================================================== - -class LogrotateStatusFileError(Exception): - ''' - Base class for exceptions in this module. - ''' - -#======================================================================== - -ZERO = timedelta(0) - -class UTC(tzinfo): - """UTC""" - - def utcoffset(self, dt): - return ZERO - - def tzname(self, dt): - return "UTC" - - def dst(self, dt): - return ZERO - -utc = UTC() - - -#======================================================================== - -class LogrotateStatusFile(object): - ''' - Class for operations with the logrotate state file - - @author: Frank Brehm - @contact: frank@brehm-online.com - ''' - - #------------------------------------------------------- - def __init__( self, file_name, - local_dir = None, - verbose = 0, - test_mode = False, - ): - ''' - Constructor. - - @param file_name: the file name of the status file - @type file_name: str - @param verbose: verbosity (debug) level - @type verbose: int - @param test_mode: test mode - no write actions are made - @type test_mode: bool - @param local_dir: The directory, where the i18n-files (*.mo) - are located. If None, then system default - (/usr/share/locale) is used. - @type local_dir: str or None - - @return: None - ''' - - self.local_dir = local_dir - ''' - @ivar: The directory, where the i18n-files (*.mo) are located. - @type: str or None - ''' - - self.t = gettext.translation( - 'LogRotateStatusFile', - local_dir, - fallback = True - ) - ''' - @ivar: a gettext translation object - @type: gettext.translation - ''' - - _ = self.t.lgettext - - self.verbose = verbose - ''' - @ivar: verbosity level (0 - 9) - @type: int - ''' - - self.file_name = file_name - ''' - @ivar: the initial file name of the status file to use - @type: str - ''' - - self.file_name_is_absolute = False - ''' - @ivar: flag, that shows, that the file name is now an absolute path - @type: bool - ''' - - self.fd = None - ''' - @ivar: the file object of the opened status file, or None, if not opened - @type: file or None - ''' - - self.was_read = False - ''' - @ivar: flag, whether the status file was read - @type: bool - ''' - - self.status_version = None - ''' - @ivar: the version of the status file (2 or 3) - @type: int or None - ''' - - self.test_mode = test_mode - ''' - @ivar: test mode - no write actions are made - @type: bool - ''' - - self.has_changed = False - ''' - @ivar: flag, whether something has changed and needs to be written - @type: bool - ''' - - self.logger = logging.getLogger('pylogrotate.status_file') - ''' - @ivar: logger object - @type: logging.getLogger - ''' - - self.file_state = {} - ''' - @ivar: the last rotation date of every particular log file - keys are the asolute filenames (without globbing) - and the values are datetime objects of the last rotation - referencing to UTC - If no rotation was made, value is datetime.min(). - @type: dict - ''' - - # Initial read and check for permissions - self.read(must_exists = False) - self._check_permissions() - - #------------------------------------------------------- - def __del__(self): - ''' - Destructor. - Enforce saving of status file, if something has changed. - ''' - - _ = self.t.lgettext - msg = _("Status file object will destroyed.") - self.logger.debug(msg) - - if self.has_changed: - self.write() - - #------------------------------------------------------- - def as_dict(self): - ''' - Transforms the elements of the object into a dict - - @return: structure as dict - @rtype: dict - ''' - - res = {} - res['local_dir'] = self.local_dir - res['t'] = self.t - res['verbose'] = self.verbose - res['file_name'] = self.file_name - res['file_name_is_absolute'] = self.file_name_is_absolute - res['fd'] = self.fd - res['status_version'] = self.status_version - res['test_mode'] = self.test_mode - res['logger'] = self.logger - res['file_state'] = self.file_state - res['was_read'] = self.was_read - res['has_changed'] = self.has_changed - - return res - - #------------------------------------------------------------ - def get_rotation_date(self, logfile): - ''' - Gives back the date of the last rotation of a particular logfile. - If this logfile is not found in the state file, datetime.min() is given back. - - @param logfile: the logfile to query - @type logfile: str - - @return: date of last rotation of this logfile - @rtype: datetime - ''' - - if not self.was_read: - self.read(must_exists = False) - - rotate_date = datetime.min.replace(tzinfo=utc) - if logfile in self.file_state: - rotate_date = self.file_state[logfile] - - return rotate_date - - #------------------------------------------------------------ - def set_rotation_date(self, logfile, rotate_date = None): - ''' - Sets the rotation date of the given logfile. - If the rotation date is not given, datetime.utcnow() is used. - - @param logfile: the logfile to set - @type logfile: str - @param rotate_date: the rotation date of this logfile - @type rotate_date: datetime or None - - @return: date of rotation of this logfile (relative to UTC) - @rtype: datetime - ''' - - date_utc = datetime.utcnow() - if rotate_date: - date_utc = rotate_date.astimezone(utc) - - _ = self.t.lgettext - msg = _("Setting rotation date of '%(file)s' to '%(date)s' ...") \ - % {'file': logfile, 'date': date_utc.isoformat(' ') } - self.logger.debug(msg) - - #self.read(must_exists = False) - self.file_state[logfile] = date_utc - self.has_changed = True - - #self.write() - - return date_utc - - #------------------------------------------------------------ - def write(self): - ''' - Writes the content of self.file_state in the state file. - - @return: success of writing - @rtype: bool - ''' - - _ = self.t.lgettext - - # setting a failing version of the status file - if not self.status_version: - self.status_version = 3 - - max_length = 1 - - # Retrieving the maximum length of the logfiles for version 3 - if self.status_version == 3: - for logfile in self.file_state: - if len(logfile) > max_length: - max_length = len(logfile) - max_length += 2 - - fd = None - # Big try block for ensure closing open status file - try: - - msg = _("Open status file '%s' for writing ...") % (self.file_name) - self.logger.debug(msg) - - # open status file for writing - if not self.test_mode: - try: - fd = open(self.file_name, 'w') - except IOError, e: - msg = _("Could not open status file '%s' for write: ") % (self.file_name) + str(e) - raise LogrotateStatusFileError(msg) - - # write logrotate version line - line = 'Logrotate State -- Version 3' - if self.status_version == 2: - line = 'logrotate state -- version 2' - if self.verbose > 2: - msg = _("Writing version line '%s'.") % (line) - self.logger.debug(msg) - line += '\n' - if fd: - fd.write(line) - - # iterate over logfiles in self.file_state - for logfile in sorted(self.file_state.keys(), lambda x,y: cmp(x.lower(), y.lower())): - rotate_date = self.file_state[logfile] - date_str = "%d-%d-%d" % (rotate_date.year, rotate_date.month, rotate_date.day) - if self.status_version == 3: - date_str = ( "%d-%02d-%02d_%02d:%02d:%02d" % - (rotate_date.year, rotate_date.month, rotate_date.day, - rotate_date.hour, rotate_date.minute, rotate_date.second)) - line = '%-*s %s' % (max_length, ('"' + logfile + '"'), date_str) - if self.verbose > 2: - msg = _("Writing line '%s'.") % (line) - self.logger.debug(msg) - if fd: - fd.write(line + "\n") - - finally: - if fd: - fd.close() - fd = None - - self.has_changed = False - return True - - #------------------------------------------------------------ - def __str__(self): - ''' - Typecasting function for translating object structure - into a string - - @return: structure as string - @rtype: str - ''' - - pp = pprint.PrettyPrinter(indent=4) - return pp.pformat(self.as_dict()) - - #------------------------------------------------------------ - def _check_permissions(self): - ''' - Checks the permissions of the state file and/or his parent directory. - Throws a LogrotateStatusFileError on a error. - - @return: success of check - @rtype: bool - ''' - - _ = self.t.lgettext - msg = _("Checking permissions of status file '%s' ...") % (self.file_name) - self.logger.debug(msg) - - if os.path.exists(self.file_name): - # Check for write access to the status file - if os.access(self.file_name, os.W_OK): - msg = _("Access to status file '%s' is OK.") % (self.file_name) - self.logger.debug(msg) - return True - else: - msg = _("No write access to status file '%s'.") % (self.file_name) - if self.test_mode: - self.logger.error(msg) - else: - raise LogrotateStatusFileError(msg) - return False - - parent_dir = os.path.dirname(self.file_name) - msg = _("Checking permissions of parent directory '%s' ...") % (parent_dir) - self.logger.debug(msg) - - # Check for existence of parent dir - if not os.path.exists(parent_dir): - msg = _("Directory '%s' doesn't exists.") % (parent_dir) - if self.test_mode: - self.logger.error(msg) - else: - raise LogrotateStatusFileError(msg) - return False - - # Check whether parent dir is a directory - if not os.path.isdir(parent_dir): - msg = _("Parent directory '%(dir)s' of status file '%(file)s' is not a directory.") \ - % {'dir': parent_dir, 'file': self.file_name } - if self.test_mode: - self.logger.error(msg) - else: - raise LogrotateStatusFileError(msg) - return False - - # Check for write access to parent dir - if not os.access(parent_dir, os.W_OK): - msg = _("No write access to parent directory '%(dir)s' of status file '%(file)s'.") \ - % {'dir': parent_dir, 'file': self.file_name } - if self.test_mode: - self.logger.error(msg) - else: - raise LogrotateStatusFileError(msg) - return False - - msg = _("Permissions to parent directory '%s' are OK.") % (parent_dir) - self.logger.debug(msg) - return True - - #------------------------------------------------------- - def read(self, must_exists = True): - ''' - Reads the status file and put the results in the dict self.file_state. - Puts back the absolute path of the status file in self.file_name on success. - - Throws a LogrotateStatusFileError on a error. - - @param must_exists: throws an exception, if true and the status file - doesn't exists - @type must_exists: bool - - @return: success of reading - @rtype: bool - ''' - - self.file_state = {} - _ = self.t.lgettext - - # Check for existence of status file - if not os.path.exists(self.file_name): - msg = _("Status file '%s' doesn't exists.") % (self.file_name) - if must_exists: - raise LogrotateStatusFileError(msg) - else: - self.logger.info(msg) - return False - - # makes the name of the status file an absolute path - if not self.file_name_is_absolute: - self.file_name = os.path.abspath(self.file_name) - self.file_name_is_absolute = True - if self.verbose > 2: - msg = _("Absolute path of status file is now '%s'.") % (self.file_name) - self.logger.debug(msg) - - # Checks, that the status file is a regular file - if not os.path.isfile(self.file_name): - msg = _("Status file '%s' is not a regular file.") % (self.file_name) - raise LogrotateStatusFileError(msg) - return False - - msg = _("Reading status file '%s' ...") % (self.file_name) - self.logger.debug(msg) - - fd = None - try: - fd = open(self.file_name, 'Ur') - except IOError, e: - msg = _("Could not read status file '%s': ") % (self.file_name) + str(e) - raise LogrotateStatusFileError(msg) - self.fd = fd - - try: - # Reading the lines of the status file - i = 0 - for line in fd: - i += 1 - line = line.strip() - if self.verbose > 4: - msg = _("Performing status file line '%(line)s' (file: '%(file)s', row: %(row)d)") \ - % {'line': line, 'file': self.file_name, 'row': i, } - self.logger.debug(msg) - - # check for file heading - if i == 1: - match = re.search(r'^logrotate\s+state\s+-+\s+version\s+([23])$', line, re.IGNORECASE) - if match: - # Correct file header - self.status_version = int(match.group(1)) - if self.verbose > 1: - msg = _("Idendified version of status file: %d") % (self.status_version) - self.logger.debug(msg) - continue - else: - # Wrong header - msg = _("Incompatible version of status file '%(file)s': %(header)s") \ - % { 'file': self.file_name, 'header': line } - fd.close() - raise LogrotateStatusFileError(msg) - - if line == '': - continue - - parts = split_parts(line) - logfile = parts[0] - rdate = parts[1] - if self.verbose > 2: - msg = _("Found logfile '%(file)s' with rotation date '%(date)s'.") \ - % { 'file': logfile, 'date': rdate } - self.logger.debug(msg) - - if logfile and rdate: - match = re.search(r'\s*(\d+)[_\-](\d+)[_\-](\d+)(?:[\s\-_]+(\d+)[_\-:](\d+)[_\-:](\d+))?', rdate) - if not match: - msg = _("Could not determine date format: '%(date)s' (file: '%(file)s', row: %(row)d)") \ - % {'date': rdate, 'file': logfile, 'row': i, } - self.logger.warning(msg) - continue - d = { - 'Y': int(match.group(1)), - 'm': int(match.group(2)), - 'd': int(match.group(3)), - 'H': 0, - 'M': 0, - 'S': 0, - } - if match.group(4) is not None: - d['H'] = int(match.group(4)) - if match.group(5) is not None: - d['M'] = int(match.group(5)) - if match.group(6) is not None: - d['S'] = int(match.group(6)) - - dt = None - try: - dt = datetime(d['Y'], d['m'], d['d'], d['H'], d['M'], d['S'], tzinfo = utc) - except ValueError, e: - msg = _("Invalid date: '%(date)s' (file: '%(file)s', row: %(row)d)") \ - % {'date': rdate, 'file': logfile, 'row': i, } - self.logger.warning(msg) - continue - - self.file_state[logfile] = dt - - else: - - msg = _("Neither a logfile nor a date found in line '%(line)s' (file: '%(file)s', row: %(row)d)") \ - % {'line': line, 'file': logfile, 'row': i, } - self.logger.warning(msg) - - finally: - fd.close - - self.fd = None - self.was_read = True - - return True - -#======================================================================== - -if __name__ == "__main__": - pass - - -#======================================================================== - -# vim: fileencoding=utf-8 filetype=python ts=4 expandtab diff --git a/logrotate.py b/logrotate.py index c252ec0..2c2f962 100755 --- a/logrotate.py +++ b/logrotate.py @@ -9,7 +9,7 @@ @contact: frank@brehm-online.com @license: GPL3 @copyright: (c) 2010-2011 by Frank Brehm, Berlin -@version: 0.2.2 +@version: 0.5.3 @summary: rotates and compress system logs ''' @@ -21,13 +21,13 @@ import os import os.path from datetime import datetime -from LogRotateGetopts import LogrotateOptParser -from LogRotateGetopts import LogrotateOptParserError +from LogRotate.Getopts import LogrotateOptParser +from LogRotate.Getopts import LogrotateOptParserError -from LogRotateHandler import LogrotateHandler -from LogRotateHandler import LogrotateHandlerError +from LogRotate.Handler import LogrotateHandler +from LogRotate.Handler import LogrotateHandlerError -import LogRotateCommon +import LogRotate.Common revision = '$Revision$' revision = re.sub( r'\$', '', revision ) @@ -36,7 +36,7 @@ revision = re.sub( r'Revision: ', r'r', revision ) __author__ = 'Frank Brehm' __copyright__ = '(C) 2011 by Frank Brehm, Berlin' __contact__ = 'frank@brehm-online.com' -__version__ = '0.5.2 ' + revision +__version__ = '0.5.3 ' + revision __license__ = 'GPL3' @@ -53,7 +53,7 @@ def main(): local_dir = None #print "Locale-Dir: %s" % ( local_dir ) - LogRotateCommon.locale_dir = local_dir + LogRotate.Common.locale_dir = local_dir t = gettext.translation('pylogrotate', local_dir, fallback=True) _ = t.lgettext