--- 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