--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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] <configfile>") + "\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
--- /dev/null
+#!/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': <file>,
+ 'to': <target>
+ },
+ 'move': [
+ ...
+ { 'from': <file2>, 'to': <file3>, 'compressed': True},
+ { 'from': <file1>, 'to': <file2>, 'compressed': True},
+ { 'from': <file0>, 'to': <file1>, '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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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] <configfile>") + "\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
+++ /dev/null
-#!/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': <file>,
- 'to': <target>
- },
- 'move': [
- ...
- { 'from': <file2>, 'to': <file3>, 'compressed': True},
- { 'from': <file1>, 'to': <file2>, 'compressed': True},
- { 'from': <file0>, 'to': <file1>, '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
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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
@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
'''
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 )
__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'
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