pvl.verkko-syslog: syslog -> irker gateway
authorTero Marttila <terom@fixme.fi>
Fri, 04 Jan 2013 14:19:05 +0200
changeset 48 40ccb8d3c96e
parent 47 eea08cf5fbc7
child 49 30c615bf7751
pvl.verkko-syslog: syslog -> irker gateway
bin/pvl.verkko-syslog
etc/syslog.conf
pvl/irker.py
pvl/syslog/args.py
pvl/syslog/rule.pp
pvl/syslog/rule.py
pvl/syslog/syslog.py
--- 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) :