pvl/dhcp/syslog.py
author Tero Marttila <terom@paivola.fi>
Mon, 09 Mar 2015 19:48:19 +0200
changeset 735 008cfe47b194
parent 213 711f71e7328b
permissions -rw-r--r--
pvl.hosts.interface: sub-interfaces for a host, which are not associated with any separate domain/network
"""
    Parse ISC dhcpd messages from pvl.syslog.
"""

from pvl.invoke import merge # XXX

import re

import logging; log = logging.getLogger('pvl.dhcp.syslog')

# TODO: use pvl.syslog.rule.SyslogRule?
class DHCPSyslogParser (object) :
    """
        Parse SyslogMessages from SyslogParser for ISC dhcp semantics.

        TODO: BOOTREQUEST from <hwaddr> via <gateway>
    """

    ## 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
        #
        # TODO: DHCPINFORM with error indicates rogue dhcp
        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

    def process (self, source) :
        """
            Process syslog items from given source, yielding parsed DHCP items.
        """

        for item in source :
            dhcp_item = self.parse(item['msg'])

            log.debug("%s: %s", item, dhcp_item)
            
            if not dhcp_item :
                # ignore
                continue

            item = merge(item, dhcp_item)

            if item.get('error') :
                item['error-type'] = self.parse_error(item['error'])
            
            yield item
    
    __call__ = process


if __name__ == '__main__' :
    import logging

    logging.basicConfig()

    import doctest
    doctest.testmod()