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