From 9a14a7ffdc58b9c2a84638dc6a6482b79795977e Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Fri, 8 Jul 2011 07:17:43 +0000 Subject: [PATCH] In Mailer mit Generierung Mail angefangen git-svn-id: http://svn.brehm-online.com/svn/my-stuff/python/PyLogrotate/trunk@282 ec8d2aa5-1599-4edb-8739-2b3a1bc399aa --- LogRotateCommon.py | 52 ++++++++++++++++ LogRotateConfig.py | 24 +++++--- LogRotateHandler.py | 85 ++++++++++++++++++++++---- LogRotateMailer.py | 124 ++++++++++++++++++++++++++++++++------ logrotate.py | 15 +++-- po/LogRotateHandler.de.po | 2 +- po/pylogrotate.de.po | 22 ++++--- po/pylogrotate.pot | 18 +++--- test/apache2 | 5 +- 9 files changed, 283 insertions(+), 64 deletions(-) diff --git a/LogRotateCommon.py b/LogRotateCommon.py index 6995471..3a13582 100755 --- a/LogRotateCommon.py +++ b/LogRotateCommon.py @@ -18,6 +18,9 @@ import sys import locale import logging import gettext +import csv +import pprint +import email.utils revision = '$Revision$' revision = re.sub( r'\$', '', revision ) @@ -427,6 +430,55 @@ def period2days(period, use_locale_radix = False, verbose = 0): 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 + #======================================================================== if __name__ == "__main__": diff --git a/LogRotateConfig.py b/LogRotateConfig.py index 6ab3137..ba613de 100755 --- a/LogRotateConfig.py +++ b/LogRotateConfig.py @@ -26,6 +26,7 @@ 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$' @@ -412,7 +413,6 @@ class LogrotateConfigurationReader(object): self.default['ifempty'] = True self.default['mailaddress'] = None self.default['mailfirst'] = None - self.default['mailfrom'] = None self.default['maxage'] = None self.default['missingok'] = False self.default['olddir'] = { @@ -1093,16 +1093,21 @@ class LogrotateConfigurationReader(object): ) return False return True - if not email_valid(val): + address_list = get_address_list(val, self.verbose) + if len(address_list): + directive['mailaddress'] = address_list + else: directive['mailaddress'] = None - self.logger.warning( ( _("Invalid Mail address '%s'.") % (val))) - return False - directive['mailaddress'] = val if self.verbose > 4: - self.logger.debug( - ( _("Setting mail address in '%(directive)s' to '%(addr)s'. (file '%(file)s', line %(lnr)s)") - % {'directive': directive_str, 'addr': val, 'file': filename, 'lnr': linenr}) - ) + 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 @@ -1945,7 +1950,6 @@ class LogrotateConfigurationReader(object): self.new_log['ifempty'] = self.default['ifempty'] self.new_log['mailaddress'] = self.default['mailaddress'] self.new_log['mailfirst'] = self.default['mailfirst'] - self.new_log['mailfrom'] = self.default['mailfrom'] self.new_log['maxage'] = self.default['maxage'] self.new_log['missingok'] = self.default['missingok'] self.new_log['olddir'] = { diff --git a/LogRotateHandler.py b/LogRotateHandler.py index 1cddd6d..dda1852 100755 --- a/LogRotateHandler.py +++ b/LogRotateHandler.py @@ -107,6 +107,7 @@ class LogrotateHandler(object): pid_file = None, mail_cmd = None, local_dir = None, + version = None, ): ''' Constructor. @@ -132,6 +133,8 @@ class LogrotateHandler(object): 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 ''' @@ -160,6 +163,14 @@ class LogrotateHandler(object): @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 @@ -254,6 +265,21 @@ class LogrotateHandler(object): @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') @@ -299,8 +325,9 @@ class LogrotateHandler(object): # define a mailer object self.mailer = LogRotateMailer( local_dir = self.local_dir, - verbose = self.verbose, + verbose = self.verbose, test_mode = self.test, + mailer_version = self.version, ) if mail_cmd: self.mailer.sendmail = mail_cmd @@ -359,6 +386,7 @@ class LogrotateHandler(object): '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, @@ -374,6 +402,7 @@ class LogrotateHandler(object): 'test': self.test, 'template': self.template, 'verbose': self.verbose, + 'version': self.version, } if self.state_file: res['state_file'] = self.state_file.as_dict() @@ -623,6 +652,10 @@ class LogrotateHandler(object): 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: @@ -649,6 +682,10 @@ class LogrotateHandler(object): _ = 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:") + \ @@ -660,7 +697,9 @@ class LogrotateHandler(object): for logfile in definition['files']: if self.verbose > 1: - msg = ( _("Performing logfile '%s' ...") % (logfile)) + "\n" + 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: @@ -673,6 +712,9 @@ class LogrotateHandler(object): continue self._rotate_file(logfile, cur_desc_index) + if self.verbose > 1: + print "\n" + return #------------------------------------------------------------ @@ -844,6 +886,11 @@ class LogrotateHandler(object): 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'.") \ @@ -962,6 +1009,8 @@ class LogrotateHandler(object): 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) @@ -981,7 +1030,7 @@ class LogrotateHandler(object): 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 + their modification time as values @type oldfiles: dict @param compress_extension: file extension for rotated and compressed logfiles @@ -1908,9 +1957,6 @@ class LogrotateHandler(object): _ = self.t.lgettext - test_mode = self.test - test_mode = False - if self.verbose > 1: msg = _("Compressing source '%(source)s' to target'%(target)s' with command '%(cmd)s'.") \ % {'source': source, 'target': target, 'cmd': command} @@ -2192,14 +2238,12 @@ class LogrotateHandler(object): _ = self.t.lgettext - test_mode = self.test - 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 test_mode: + if not self.test: # open source for reading f_in = None try: @@ -2265,14 +2309,12 @@ class LogrotateHandler(object): _ = self.t.lgettext - test_mode = self.test - 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 test_mode: + if not self.test: # open source for reading f_in = None try: @@ -2318,6 +2360,25 @@ class LogrotateHandler(object): 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__": diff --git a/LogRotateMailer.py b/LogRotateMailer.py index 7e5951b..8eb20c4 100755 --- a/LogRotateMailer.py +++ b/LogRotateMailer.py @@ -22,8 +22,15 @@ import os import os.path import pwd import socket +import csv +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.text import MIMEText from LogRotateCommon import email_valid @@ -60,18 +67,21 @@ class LogRotateMailer(object): 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 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 ''' @@ -155,6 +165,14 @@ class LogRotateMailer(object): @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 @@ -349,17 +367,18 @@ class LogRotateMailer(object): ''' 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['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 @@ -417,6 +436,73 @@ class LogRotateMailer(object): return + #------------------------------------------------------- + def send_file(self, filename, addresses, original=None, + mime_type='text/plain'): + ''' + 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 + + @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) + + 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)) + + 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) + + composed = mail_container.as_string() + if self.verbose > 2: + msg = _("Generated E-mail:") + "\n" + composed + self.logger.debug(msg) + + return True + #======================================================================== if __name__ == "__main__": diff --git a/logrotate.py b/logrotate.py index 65cb030..695cf5a 100755 --- a/logrotate.py +++ b/logrotate.py @@ -36,7 +36,7 @@ revision = re.sub( r'Revision: ', r'r', revision ) __author__ = 'Frank Brehm' __copyright__ = '(C) 2011 by Frank Brehm, Berlin' __contact__ = 'frank@brehm-online.com' -__version__ = '0.2.2 ' + revision +__version__ = '0.5.1 ' + revision __license__ = 'GPL3' @@ -95,7 +95,7 @@ def main(): % {'prog': cur_proc, 'date': datetime.now().isoformat(' '), } ) + "\n" - sep_line = '-' * 79 + sep_line = '=' * 79 if testmode: print _("Test mode is ON.") @@ -124,6 +124,7 @@ def main(): pid_file = opt_parser.options.pidfile, mail_cmd = opt_parser.options.mailcmd, local_dir = local_dir, + version = __version__, ) except LogrotateHandlerError, e: sys.stderr.write(str(e) + "\n") @@ -144,13 +145,19 @@ def main(): print "" if verbose_level > 0: print sep_line + "\n" - print _("Stage 3: deleting of old logfiles") + "\n" + print _("Stage 3: sending logfiles per mail") + "\n" + lr_handler.send_logfiles() + + print "" + if verbose_level > 0: + print sep_line + "\n" + print _("Stage 4: deleting of old logfiles") + "\n" lr_handler.delete_oldfiles() print "" if verbose_level > 0: print sep_line + "\n" - print _("Stage 4: compression of old log files") + "\n" + print _("Stage 5: compression of old log files") + "\n" lr_handler.compress() lr_handler = None diff --git a/po/LogRotateHandler.de.po b/po/LogRotateHandler.de.po index e766e43..3bb0456 100644 --- a/po/LogRotateHandler.de.po +++ b/po/LogRotateHandler.de.po @@ -339,7 +339,7 @@ msgstr "Gefundene Logdateien zum Löschen:" #: LogRotateHandler.py:1102 msgid "No old logfiles to delete found." -msgstr "Keile Logdateien zum Löschen gefunden." +msgstr "Keine Logdateien zum Löschen gefunden." #: LogRotateHandler.py:1132 #, python-format diff --git a/po/pylogrotate.de.po b/po/pylogrotate.de.po index 4e4a5d5..24f8c43 100644 --- a/po/pylogrotate.de.po +++ b/po/pylogrotate.de.po @@ -54,23 +54,27 @@ msgstr "Nur Überprüfung der Konfiguration." msgid "Stage 1: reading configuration" msgstr "Phase 1: Einlesen der Konfiguration" -#: logrotate.py:125 +#: logrotate.py:133 msgid "Handler object structure" msgstr "Struktur des Handlerobjektes" -#: logrotate.py:133 +#: logrotate.py:141 msgid "Stage 2: underlying log rotation" msgstr "Phase 2: Eigentliches Rotieren" -#: logrotate.py:139 -msgid "Stage 3: deleting of old logfiles" -msgstr "Phase 3: Löschen der alten Logdateien" +#: logrotate.py:147 +msgid "Stage 3: sending logfiles per mail" +msgstr "Phase 3: Verschicken von Logdateien per Mail" + +#: logrotate.py:153 +msgid "Stage 4: deleting of old logfiles" +msgstr "Phase 4: Löschen der alten Logdateien" -#: logrotate.py:145 -msgid "Stage 4: compression of old log files" -msgstr "Phase 4: Komprimieren der alten Logdateien" +#: logrotate.py:159 +msgid "Stage 5: compression of old log files" +msgstr "Phase 5: Komprimieren der alten Logdateien" -#: logrotate.py:152 +#: logrotate.py:166 #, python-format msgid "[%(date)s]: %(prog)s ended logrotation." msgstr "[%(date)s]: %(prog)s hat Logrotation beendet." diff --git a/po/pylogrotate.pot b/po/pylogrotate.pot index 759c530..5848fba 100644 --- a/po/pylogrotate.pot +++ b/po/pylogrotate.pot @@ -54,23 +54,27 @@ msgstr "" msgid "Stage 1: reading configuration" msgstr "" -#: logrotate.py:125 +#: logrotate.py:133 msgid "Handler object structure" msgstr "" -#: logrotate.py:133 +#: logrotate.py:141 msgid "Stage 2: underlying log rotation" msgstr "" -#: logrotate.py:139 -msgid "Stage 3: deleting of old logfiles" +#: logrotate.py:147 +msgid "Stage 3: sending logfiles per mail" +msgstr "" + +#: logrotate.py:153 +msgid "Stage 4: deleting of old logfiles" msgstr "" -#: logrotate.py:145 -msgid "Stage 4: compression of old log files" +#: logrotate.py:159 +msgid "Stage 5: compression of old log files" msgstr "" -#: logrotate.py:152 +#: logrotate.py:166 #, python-format msgid "[%(date)s]: %(prog)s ended logrotation." msgstr "" diff --git a/test/apache2 b/test/apache2 index a6129d8..108ee95 100644 --- a/test/apache2 +++ b/test/apache2 @@ -46,8 +46,9 @@ endscript delaycompress #start 1 daily - maxage 1y - mail test@uhu-banane.de + #maxage 1y + maxage 1d + mail test@uhu-banane.de, Frank Brehm , "Brehm, Frank" noolddir postrotate apache_restart } -- 2.39.5