--- a/bin/pvl.verkko-syslog Thu Jan 03 13:14:29 2013 +0200
+++ b/bin/pvl.verkko-syslog Fri Jan 04 14:19:05 2013 +0200
@@ -1,13 +1,15 @@
#!/usr/bin/env python
"""
- Monitor DHCP use.
+ Syslog -> Irk
"""
__version__ = '0.0'
import pvl.args
import pvl.syslog.args
+import pvl.syslog.rule
+import pvl.irker
import logging, optparse
@@ -31,7 +33,14 @@
# options
parser.add_option_group(pvl.args.parser(parser))
+
+ # input
parser.add_option_group(pvl.syslog.args.parser(parser))
+ parser.add_option_group(pvl.irker.parser(parser))
+
+ # processing
+ parser.add_option('-r', '--rules', metavar='FILE',
+ help="Syslog rules")
# parse
options, args = parser.parse_args(argv[1:])
@@ -41,32 +50,33 @@
return options, args
-# prototype
-import pvl.syslog.rule
-
-sudo = pvl.syslog.rule.SyslogRule('sudo',
- program = 'sudo',
- pattern = r'\s*(?P<login>\S+) : TTY=(?P<tty>\S+) ; PWD=(?P<pwd>.+?) ; USER=(?P<user>\S+) ; COMMAND=(?P<command>.*)',
- format = "{login}:{tty} - {user}@{host}:{pwd} - {command!r}",
-)
-
-sudo_env = pvl.syslog.rule.SyslogRule('sudo',
- program = 'sudo',
- pattern = r'\s*(?P<login>\S+) : TTY=(?P<tty>\S+) ; PWD=(?P<pwd>.+?) ; USER=(?P<user>\S+) ; ENV=(?P<env>.+?) ; COMMAND=(?P<command>.*)',
- format = "{login}:{tty} - {user}@{host}:{pwd} - {env} {command!r}",
-)
-
-rules = pvl.syslog.rule.SyslogRules((sudo_env, sudo))
-
def main (argv) :
options, args = parse_options(argv)
+
+ if args :
+ # XXX: targets
+ target, = args
+ else :
+ target = None
+ log.info("Open syslog...")
syslog = pvl.syslog.args.apply(options)
+
+ log.info("Load rules...")
+ rules = pvl.syslog.rule.config(open(options.rules))
+
+ log.info("Connect IRK..")
+ irk, target = pvl.irker.apply(options, target=target)
+
+ if not target :
+ log.error("No irk target given")
+ return 2
- log.info("Start processing syslog messages...")
-
+ log.info("Process syslog messages...")
for tag, line in rules.process(syslog.loop()) :
- print tag, line
+ # TODO: map tag -> target?
+ log.info("%s", line)
+ target(line)
# done
log.info("Exiting...")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/etc/syslog.conf Fri Jan 04 14:19:05 2013 +0200
@@ -0,0 +1,34 @@
+[puppet_readshadow]
+program = sudo
+pattern = \s*(?P<login>puppet) : TTY=(?P<tty>\S+)\s; PWD=(?P<pwd>.+?)\s; USER=(?P<user>root)\s; COMMAND=(?P<command>/usr/bin/getent shadow \w+)
+format =
+
+[sudo]
+program = sudo
+pattern = \s*(?P<login>\S+) : TTY=(?P<tty>\S+)\s; PWD=(?P<pwd>.+?)\s; USER=(?P<user>\S+)\s; COMMAND=(?P<command>.*)
+format = {login}:{tty} - {user}@{host}:{pwd} - {command!r}
+
+[sudo_env]
+program = sudo
+pattern = \s*(?P<login>\S+) : TTY=(?P<tty>\S+) ; PWD=(?P<pwd>.+?) ; USER=(?P<user>\S+) ; ENV=(?P<env>.+?) ; COMMAND=(?P<command>.*)
+format = {login}:{tty} - {user}@{host}:{pwd} - {env} {command!r}
+
+[ssh]
+program = sshd
+pattern = \s*Accepted password for (?P<user>\S+) from (?P<ip>\S+) port (?P<port>\S+) (?P<proto>\S+)
+format = SSH login for {user}@{host} from {ip}
+
+[cron]
+program = cron
+
+[su_nobody]
+program = su
+pattern = Successful su for nobody by root|\+ \?\?\? root:nobody
+#flags = re.IGNORECASE
+
+[puppet]
+program = puppet
+format = {host} {msg}
+
+[all]
+format = {host} {msg}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/irker.py Fri Jan 04 14:19:05 2013 +0200
@@ -0,0 +1,158 @@
+import optparse, sys
+
+import logging; log = logging.getLogger('pvl.irker')
+
+# proto
+import socket, json
+
+def parser (parser, connect='tcp://localhost/', target=None) :
+ """
+ Optparse option group.
+ """
+
+ irker = optparse.OptionGroup(parser, 'Irker output')
+
+ irker.add_option('--irker-connect', metavar='URL', default=connect,
+ help="Irker daemon URL")
+
+ irker.add_option('--irker-target', metavar='IRC', default=target,
+ help="Irker target URL")
+
+ return irker
+
+def apply (options, target=None) :
+ """
+ Return Irker (XXX: target) from options.
+ """
+
+ # XXX: None -> stdout
+ irk = Irker(options.irker_connect)
+
+ if options.irker_target :
+ target = options.irker_target
+
+ if target :
+ target = irk.target(target)
+
+ return irk, target
+
+def connect (host=None, port=None, family=socket.AF_UNSPEC, socktype=socket.SOCK_STREAM) :
+ for af, st, proto, name, addr in socket.getaddrinfo(host, port, family, socktype) :
+ try :
+ s = socket.socket(af, st, proto)
+
+ except socket.error as error :
+ log.warning("%s:%s: socket: %s", host, port, error)
+
+ log.info("%s", name)
+
+ try :
+ s.connect(addr)
+
+ except socket.error as error :
+ log.warning("%s:%s: connect: %s", host, port, error)
+
+ return s
+
+ else :
+ raise Exception("Unable to connect: %s:%s" % (host, port))
+
+class Irk (object) :
+ """
+ Irker connection.
+ """
+
+ PORT = 6659
+
+ @classmethod
+ def socket (cls, socket) :
+ return cls(socket.makefile('w'))
+
+ def __init__ (self, file) :
+ self.file = file
+
+ log.debug("%s", file)
+
+ def send (self, **opts) :
+ json.dump(opts, self.file)
+ self.file.write('\n')
+ self.file.flush()
+
+ def join (self, to) :
+ self.send(to=to, privmsg='')
+
+ def privmsg (self, to, *args) :
+ for arg in args :
+ self.send(to=to, privmsg=arg)
+
+import urlparse
+import functools
+
+class Irker (object) :
+ """
+ Reconnecting irker.
+
+ XXX: reconnect with state, or just crash and burn to be restarted?
+ """
+
+ SCHEME = {
+ 'tcp': (socket.AF_INET, socket.SOCK_STREAM),
+ 'udp': (socket.AF_INET, socket.SOCK_DGRAM),
+ 'unix': (socket.AF_UNIX, socket.SOCK_DGRAM),
+ }
+
+ def __init__ (self, url=None) :
+ if url :
+ self.url = urlparse.urlparse(url)
+ else :
+ self.url = None
+
+ self.targets = {}
+
+ self.irk = self.connect()
+
+ def connect (self) :
+ if not self.url :
+ # XXX: not here, do this in apply()?
+ return Irk(sys.stdout)
+
+ family, socktype = self.SCHEME[self.url.scheme]
+
+ if family == socket.AF_UNIX :
+ raise Exception("unix:// is not supported")
+ else :
+ # inet
+ s = connect(self.url.hostname, self.url.port or Irk.PORT, family=family, socktype=socktype)
+
+ irk = Irk.socket(s)
+
+ # rejoin
+ for target in self.targets :
+ irk.join(target)
+
+ return irk
+
+ def _target (self, target, *args) :
+ try :
+ for msg in args :
+ self.irk.send(to=target, privmsg=msg)
+ except IOError as ex :
+ log.warning("lost irk: %s", ex)
+
+ # XXX: reconnect?
+ self.irk = self.connect()
+
+ def target (self, target) :
+ """
+ Bind to given target URL, returning a callable for sending messages.
+ """
+
+ if target in self.targets :
+ _target = self.targets[target]
+ else :
+ self.irk.join(target)
+ _target = self.targets[target] = functools.partial(self._target, target)
+
+ return _target
+
+
--- a/pvl/syslog/args.py Thu Jan 03 13:14:29 2013 +0200
+++ b/pvl/syslog/args.py Fri Jan 04 14:19:05 2013 +0200
@@ -61,8 +61,9 @@
return None
else :
- log.error("No --syslog source given")
- sys.exit(2)
+ # from stdin
+ source = sys.stdin
+ poll = False # XXX: SyslogSource can't handle normal file EOF
parser = SyslogParser(
raw = options.syslog_raw,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/syslog/rule.pp Fri Jan 04 14:19:05 2013 +0200
@@ -0,0 +1,46 @@
+class SyslogRule (SyslogFilter) :
+ """
+ A rule matches syslog lines, and formats them.
+
+ tag - apply given tag to matches
+ """
+
+ def __init__ (self, tag, program=None, pattern=None, format=None, flags=None, **opts) :
+ if pattern and not isinstance(pattern, re.RegexObject) :
+ pattern = re.compile(pattern, flags)
+
+ super(SyslogRule, self).__init__(prog=program)
+
+ self.tag = tag
+ self.format = format
+ self.pattern = pattern
+
+ def apply (self, item) :
+ """
+ Apply rule against given item.
+ """
+
+ # filter
+ match = self.filter(item)
+
+ if not match :
+ # ignore
+ return None
+
+ match = self.pattern.match(item['msg'])
+
+ if not match :
+ # ignore
+ return None
+
+ # apply
+ item.update(match.groupdict())
+
+ if self.tag is False :
+ # drop
+ return False
+
+ if self.format :
+ # return
+ return self.tag, self.format.format(**item)
+
--- a/pvl/syslog/rule.py Thu Jan 03 13:14:29 2013 +0200
+++ b/pvl/syslog/rule.py Fri Jan 04 14:19:05 2013 +0200
@@ -12,7 +12,10 @@
"""
def __init__ (self, tag, program=None, pattern=None, format=None, flags=0) :
- pattern = re.compile(pattern, flags)
+ log.debug("%s: %s", tag, pattern)
+
+ if pattern :
+ pattern = re.compile(pattern, flags)
self.filter = SyslogFilter(prog=program)
@@ -33,8 +36,6 @@
# filter
match = self.filter.filter(item)
- log.debug("filter: %s", match)
-
if not match :
# ignore
return None
@@ -57,6 +58,9 @@
# return
return self.tag, self.format.format(**item)
+ def __str__ (self) :
+ return self.tag
+
class SyslogRules (object) :
"""
Apply a set of rules against lines.
@@ -71,7 +75,14 @@
"""
for rule in self.rules :
- match = rule.apply(item)
+ try :
+ match = rule.apply(item)
+
+ except Exception as ex :
+ log.exception("rule %s: %r", rule, item)
+ continue
+
+ log.debug("%s: %s", rule, match)
if match is None :
continue
@@ -91,3 +102,24 @@
if match :
yield match
+# XXX: ConfigParser kinda sucks
+import ConfigParser
+
+def config (file) :
+ """
+ Load SyslogRules from config.
+ """
+
+ config = ConfigParser.RawConfigParser()
+ config.readfp(file)
+
+ return SyslogRules(tuple(config_rules(config)))
+
+def config_rules (config) :
+ """
+ Yield SyslogRule's from given config.
+ """
+
+ for section in config.sections() :
+ yield SyslogRule(section, **dict(config.items(section)))
+
--- a/pvl/syslog/syslog.py Thu Jan 03 13:14:29 2013 +0200
+++ b/pvl/syslog/syslog.py Fri Jan 04 14:19:05 2013 +0200
@@ -28,6 +28,8 @@
self.parser = parser
self.filter = filter
+ log.debug("source: %s", source)
+
self._poll = poll
def __iter__ (self) :
@@ -65,7 +67,7 @@
elif timeout is False :
timeout = 0.0
- log.debug("%f", timeout)
+ log.debug("%s", timeout)
# select
read, write, ex = select.select(read, write, [], timeout)
@@ -88,8 +90,11 @@
# mainloop
while True :
+ log.debug("tick")
+
# pull in messages
for item in self :
+ log.debug("%s", item)
yield item
# poll
@@ -100,8 +105,6 @@
# done
break
- log.debug("tick")
-
log.debug("exit")
def stop (self) :