pvl/syslog/dhcp.py
changeset 31 3e6d0feb115c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/syslog/dhcp.py	Wed Oct 24 21:02:33 2012 +0300
@@ -0,0 +1,191 @@
+"""
+    Parse ISC dhcpd messages in syslog.
+"""
+
+import re
+
+class DHCPSyslogFilter (object) :
+    """
+        Parse SyslogMessages from SyslogParser for ISC dhcp semantics.
+    """
+
+    ## various message types sent/recieved by dhcpd
+    # from server/dhcp.c
+    TYPE_NAMES = (
+        "DHCPDISCOVER",
+        "DHCPOFFER",
+        "DHCPREQUEST",
+        "DHCPDECLINE",
+        "DHCPACK",
+        "DHCPNAK",
+        "DHCPRELEASE",
+        "DHCPINFORM",
+        "type 9",
+        "DHCPLEASEQUERY",
+        "DHCPLEASEUNASSIGNED",
+        "DHCPLEASEUNKNOWN",
+        "DHCPLEASEACTIVE"
+    )
+
+    # message-parsing regexp..
+    RECV_MESSAGE_RE = (
+        # dhcpdiscover/ack_lease: info/error
+        #   hwaddr:     <no identifier>
+        #   hostname:   Hostname Unsuitable for Printing
+        #   error:
+        #               peer holds all free leases
+        #               network %s: no free leases
+        re.compile(r'(?P<type>DHCPDISCOVER) from (?P<hwaddr>.+?)( \((?P<hostname>.+?)\))? via (?P<gateway>.+?)(: (?P<error>.+?))?$'),
+
+        # dhcprequest
+        #   error:
+        #               wrong network.
+        #               ignored (not authoritative).
+        #               ignored (unknown subnet).
+        #               lease %s unavailable.
+        #               unknown lease %s.
+        re.compile(r'(?P<type>DHCPREQUEST) for (?P<lease>.+?)( \((?P<server>.+?)\))? from (?P<hwaddr>.+?)( \((?P<hostname>.+?)\))? via (?P<gateway>.+?)(: (?P<error>.+?))?$'),
+
+        # dhcprelease
+        re.compile(r'(?P<type>DHCPRELEASE) of (?P<lease>.+?) from (?P<hwaddr>.+?)( \((?P<hostname>.+?)\))? via (?P<gateway>.+?) \((?P<found>.+?)\)$'),
+
+        # dhcpdecline
+        #   status:
+        #       abandoned
+        #       not found
+        #       ignored
+        re.compile(r'(?P<type>DHCPDECLINE) of (?P<lease>.+?) from (?P<hwaddr>.+?)( \((?P<hostname>.+?)\))? via (?P<gateway>.+?): (?P<status>.+?)$'),
+
+        # dhcpinform
+        #   error:
+        #       ignored (null source address).
+        #       unknown subnet for relay address %s
+        #       unknown subnet for %s address %s
+        #       not authoritative for subnet %s
+        re.compile(r'(?P<type>DHCPINFORM) from (?P<lease>.+?) via (?P<gateway>.+?)(: (?P<error>.+?))?$'),
+        
+        # dhcpleasequery
+        re.compile(r'(?P<type>DHCPLEASEQUERY) from (?P<server>.+?)( for (?P<key_type>IP|client-id|MAC address) (?P<key>.+?))?(: (?P<error>.+?))?$'),
+
+        # dhcp: generic/unknown packet
+        re.compile(r'(?P<type>\w+) from (?P<hwaddr>.+?) via (?P<gateway>.+?): (?P<error>.+?)$'),
+    )
+
+    SEND_MESSAGE_RE = (
+        # dhcp_reply
+        re.compile(r'(?P<type>DHCPACK|DHCPOFFER|BOOTREPLY) on (?P<lease>.+?) to (?P<hwaddr>.+?)( \((?P<hostname>.+?)\))? via (?P<gateway>.+?)$'),
+
+        # dhcpinform
+        #   hwaddr:     <no client hardware address>
+        re.compile(r'(?P<type>DHCPACK) to (?P<lease>.+?) \((?P<hwaddr>.+?)\) via (?P<gateway>.+?)$'),
+
+        # nak_lease
+        re.compile(r'(?P<type>DHCPNAK) on (?P<lease>.+?) to (?P<hwaddr>.+?) via (?P<gateway>.+?)$'),
+
+        # dhcpleasequery
+        re.compile(r'(?P<type>DHCPLEASEUNKNOWN|DHCPLEASEACTIVE|DHCPLEASEUNASSIGNED) to (?P<lease>.+?) for (?P<key_type>IP|client-id|MAC address) (?P<key>.+?) \((?P<count>\d+) associated IPs\)$'),
+    )
+
+    MESSAGE_ERROR_RE = (
+        ('peer-all-free-leases',    re.compile('peer holds all free leases')),
+        ('no-free-leases',          re.compile(r'network (?P<network>.+?): no free leases')),
+        ('wrong-network',           re.compile(r'wrong network')),
+        ('ignored-not-auth',        re.compile(r'ignored \(not authoritative\)')),
+        ('ignored-unknown-subnet',  re.compile(r'ignored \(unknown subnet\)')),
+        ('lease-unavailable',       re.compile(r'lease (?P<lease>.+?) unavailable')),
+        ('lease-unknown',           re.compile(r'unknown lease (?P<lease>.+?).$')),
+    )
+
+    ERROR_RE = (
+        # find_lease
+        ('duplicate-uid-lease', 
+            re.compile(r'uid lease (?P<client>.+?) for client (?P<hwaddr>.+?) is duplicate on (?P<shared_network>.+?)$')),
+
+        # dhcprelease
+        ('dhcprelease-requested-address', 
+            re.compile(r'DHCPRELEASE from (?P<hwaddr>.+?) specified requested-address.')),
+
+        # ???
+        ('unexpected-icmp-echo-reply',
+            re.compile(r'unexpected ICMP Echo Reply from (?P<client>.+?)$')),
+        
+        ('host-unknown',
+            re.compile(r'(?P<host>.+?): host unknown.')),
+    )
+
+    IGNORE_RE = (
+        re.compile(r'Wrote (?P<count>\d+) (?P<what>.+?) to leases file.'),
+    )
+
+    def parse (self, line) :
+        """
+            Match line against our regexps, returning a
+
+                {
+                    tag:        send/recv/error,
+                    type:       ...,
+                    [error]:    ...,
+                    ...
+                }
+
+            dict if matched
+
+            Returns False if the message is ignored, or None if the no regexp matched.
+        """
+
+        for tag, re_list in (
+            ('recv',    self.RECV_MESSAGE_RE),
+            ('send',    self.SEND_MESSAGE_RE),
+        ) :
+            for re in re_list :
+                # test
+                match = re.match(line)
+
+                if match :
+                    data = match.groupdict()
+                    data['tag'] = tag
+
+                    return data
+                
+        # error?
+        for type, re in self.ERROR_RE:
+            match = re.match(line)
+
+            if match : 
+                data = match.groupdict()
+                data['tag'] = 'error'
+                data['type'] = type
+
+                return data
+
+        # ignore
+        for re in self.IGNORE_RE :
+            if re.match(line) :
+                # ignore
+                return False
+
+        # unknown
+        return None
+
+    def parse_error (self, error) :
+        """
+            Match given error status from send/recv against known types, returning a type name or None.
+        """
+
+        for type, re in self.MESSAGE_ERROR_RE :
+            match = re.match(error)
+
+            if match :
+                return type
+        
+        # nope
+        return None
+
+if __name__ == '__main__' :
+    import logging
+
+    logging.basicConfig()
+
+    import doctest
+    doctest.testmod()
+