work on logwatch docs, small tweaks
authorTero Marttila <terom@fixme.fi>
Thu, 04 Feb 2010 20:39:53 +0200
changeset 48 ba101beeb062
parent 47 81b37185209c
child 49 666e638059b2
child 50 edbc337b7c29
work on logwatch docs, small tweaks
etc/fixbot-logwatch.py
fixbot/logwatch/__init__.py
fixbot/logwatch/filters.py
fixbot/logwatch/sources.py
--- a/etc/fixbot-logwatch.py	Thu Feb 04 20:14:22 2010 +0200
+++ b/etc/fixbot-logwatch.py	Thu Feb 04 20:39:53 2010 +0200
@@ -12,6 +12,10 @@
 ## Iterable of LogSource objects
 logwatch_sources = (
     Fifo("test", os.path.join(logwatch_dir, "test.fifo"), (
+        filters.sudo,
+        filters.ssh,
+        filters.cron_killer,
+        filters.su_nobody_killer,
         filters.all,
     )),
 )
--- a/fixbot/logwatch/__init__.py	Thu Feb 04 20:14:22 2010 +0200
+++ b/fixbot/logwatch/__init__.py	Thu Feb 04 20:39:53 2010 +0200
@@ -1,3 +1,8 @@
+"""
+    A module that can monitor any number of log files, and then process each incoming line, filtering and then possibly
+    reformatting them before sending out to IRC.
+"""
+
 from fixbot import api
 
 class LogWatchModule (api.Module) :
@@ -20,6 +25,10 @@
         self.sources = config['logwatch-sources']
     
     def handleConnect (self) :
+        """
+            Initialize each source
+        """
+
         for source in self.sources :
             source.setModule(self)
     
--- a/fixbot/logwatch/filters.py	Thu Feb 04 20:14:22 2010 +0200
+++ b/fixbot/logwatch/filters.py	Thu Feb 04 20:39:53 2010 +0200
@@ -1,66 +1,121 @@
 import re
 
-class FullFilter (object) :
+class BaseFilter (object) :
+    """
+        A filter object matches incoming lines, to determine how they are handled, classify them, and optionally reformat them
+    """
+    
+    # the LogWatchModule event to send
+    event_type = None
+    
     def __init__ (self, event_type) :
         self.event_type = event_type
 
     def test (self, line) :
-        return line
+        """
+            Match against the given line, and return one of:
 
-class NullFilter (object) :
+                None        - filter did not match, continue
+                False       - filter matched, line should be dropped
+                    (type, <str>)
+                            - filter matched, pass formatted output
+        """
+
+        raise NotImplementedError()
+
+class FullFilter (BaseFilter) :
+    """
+        A trivial filter that matches every possible line as-is
+    """
+
+    def test (self, line) :
+        # pass through
+        return self.event_type, line
+
+class NullFilter (BaseFilter) :
+    """
+        A filter that drops every line matching a given regexp
+    """
+    
     def __init__ (self, pattern, flags=None) :
+        # don't need an event_type
+
         self.regexp = re.compile(pattern, flags)
     
     def test (self, line) :
         match = self.regexp.search(line)
         
         if match :
+            # drop
             return False
 
-class SimpleFilter (object) :
-    def __init__ (self, event_type, pattern, format) :
-        self.event_type = event_type
+class SimpleFilter (BaseFilter) :
+    """
+        A simple filter that passes through any lines that match, optionally reformatting them with the given string
+        pattern, using the regexp match groups as parameters.
+    """
 
+    def __init__ (self, event_type, pattern, format=None) :
+        super(SimpleFilter, self).__init__(event_type)
+        
+        # store
         self.regexp = re.compile(pattern)
         self.format = format
 
     def test (self, line) :
+        # match
         match = self.regexp.search(line)
         
-        if match :
-            return self._filter(match)
+        if not match :
+            # continue
+            return None
         
-    def _filter (self, match) :
-        return self.format % match.groupdict()
+        # reformat?
+        if self.format :
+            # format with regexp match groups
+            return self.event_type, self.format % match.groupdict()
+        
+        else :
+            # match as-is
+            return self.event_type, line
 
+# matches a timestamp prefix
 _timestamp = "\w{3} [0-9 ]\d \d{2}:\d{2}:\d{2}"
 
+
+# matches all lines
 all = FullFilter("all")
 
+# match all lines, but drop the prefixed timestamp
 all_wo_timestamps = SimpleFilter(
     "all",
     "^" + _timestamp + " (?P<line>.+)$",
     "%(line)s"
 )
 
+# match sudo invocations, reformatting them nicely
 sudo = SimpleFilter(
     "sudo",
     "(?P<hostname>\S+)\s+sudo:\s*(?P<username>\S+) : TTY=(?P<tty>\S+) ; PWD=(?P<pwd>.+?) ; USER=(?P<target_user>\S+) ; COMMAND=(?P<command>.*)",
     "%(username)s:%(tty)s - %(target_user)s@%(hostname)s:%(pwd)s - %(command)r"
 )
 
+# match accepted ssh logins
 ssh = SimpleFilter(
     "ssh",
-    "(?P<success>Accepted|Failed) password for (?P<username>\S+) from (?P<ip>\S+) port (?P<port>\S+) (?P<proto>\S+)",
-    "%(success)s login for %(username)s from %(ip)s:%(port)s proto %(proto)s"
+    "Accepted password for (?P<username>\S+) from (?P<ip>\S+) port (?P<port>\S+) (?P<proto>\S+)",
+    "SSH login for %(username)s from %(ip)s:%(port)s"
 )
 
+# drops pam output from cron
 cron_killer = NullFilter(
         "^" + _timestamp + " \S+\s+(CRON|su)\[\d+\]: pam_unix\(cron:\w+\): session (opened|closed) for user \w+( by \(uid=\d+\))?$",
         re.IGNORECASE
 )
 
+# drops `su nobody` output (from cron)
 su_nobody_killer = NullFilter(
     "^" + _timestamp + " \S+\s+su\[\d+\]: (Successful su for nobody by root|\+ \?\?\? root:nobody)$",
     re.IGNORECASE
 )
+
--- a/fixbot/logwatch/sources.py	Thu Feb 04 20:14:22 2010 +0200
+++ b/fixbot/logwatch/sources.py	Thu Feb 04 20:39:53 2010 +0200
@@ -1,10 +1,23 @@
-from twisted.internet import reactor, protocol
+"""
+    Implementations of the various sources of log data
+"""
+
+from twisted.internet import protocol
 from twisted.python import log
 
 from fixbot import fifo
 
 class LogSource (object) :
+    """
+        Reads lines of log data from some file or other source.
+    """
+
     def __init__ (self, name, filters) :
+        """
+            name            - label lines read from this source
+            filters         - LogFilter chain to pass lines through
+        """
+
         # set later on
         self.module = None
         
@@ -25,6 +38,10 @@
         self.module.error(msg)
 
     def handleData (self, data) :
+        """
+            Buffer the given chunk of data, passing any full lines to handleLine
+        """
+
         data = self.buf + data
         
         while "\n" in data :
@@ -42,20 +59,30 @@
             out = filter.test(line)
 
             if out :
+                # unpack
+                type, msg = out
+
                 # positive match, send
-                log.msg("\t%s: %s" % (filter.event_type, out))
-                self.module.sendEvent(filter.event_type, out)
-
+                log.msg("\t%s: %s" % (type, msg))
+                
+                # drop until we have a module
+                if self.module :
+                    self.module.sendEvent(type, msg)
+                
+                # ok, first hit does it
                 break
 
             elif out is False :
                 # negative match, stop processing
                 return
 
-            else :  # None
+            elif out is None :
                 # no match
                 continue
 
+            else :
+                raise ValueError(out)
+
 class File (LogSource, protocol.ProcessProtocol) :
     """
         Stream lines from a regular file using /usr/bin/tail -f