"""
DHCP dhcpd.leases handling/tracking
"""
import pvl.syslog.tail # TailFile
from datetime import datetime
from pvl.dhcp.config import DHCPConfigParser
import logging; log = logging.getLogger('pvl.dhcp.leases')
DHCPD_LEASES = '/var/lib/dhcp/dhcpd.leases'
class DHCPLeases (object) :
"""
Process log-structured leases file, updated by dhcpd.
"""
LEASE_DATES = ('starts', 'ends', 'tstp', 'tsfp', 'atsfp', 'cltt')
# default format
LEASE_DATE_NEVER = 'never'
LEASE_DATE_FMT_DEFAULT = '%w %Y/%m/%d %H:%M:%S'
lease_date_fmt = LEASE_DATE_FMT_DEFAULT
def __init__ (self, path=DHCPD_LEASES) :
"""
path - path to dhcpd.leases file
"""
# tail; handles file re-writes
self.source = pvl.syslog.tail.Tail(path)
# parser state
self.parser = DHCPConfigParser()
# initial leases state
self._leases = None
def reset (self) :
"""
Reset state, if we started to read a new file.
"""
self._leases = {}
def process_lease_item_date (self, args) :
"""
Process lease-item date spec into datetime.
Returns None if 'never'.
"""
data = ' '.join(args)
if data == self.LEASE_DATE_NEVER :
return None
else :
return datetime.strptime(data, self.lease_date_fmt)
def process_lease_item (self, lease, item) :
"""
Process a single item from the lease, updating the lease dict
"""
item = list(item)
name = item.pop(0)
subname = item[0] if item else None
if name in self.LEASE_DATES :
lease[name] = self.process_lease_item_date(item)
elif name == 'hardware':
# args
lease['hwtype'], lease['hwaddr'] = item
elif name == 'uid' :
lease['uid'], = item
elif name == 'client-hostname' :
lease['client-hostname'], = item
elif name == 'abandoned' :
lease['abandoned'] = True
elif name == 'binding' :
_state, lease['binding-state'] = item
elif name == 'next' and subname == 'binding' :
_binding, _state, lease['next-binding-state'] = item
else :
log.warn("unknown lease item: %s: %s", name, item)
def process_lease (self, lease_name, items) :
"""
Process given lease block to update our state.
Returns the lease object, and a possible old lease.
"""
# replace any existing
lease = self._leases[lease_name] = {}
# meta
lease['lease'] = lease_name
# parse items
for item in items :
try :
self.process_lease_item(lease, item)
except Exception as ex:
log.warn("Failed to process lease item: %s: %s:", lease_name, item, exc_info=True)
# k
log.debug("%-15s: %s", lease_name, lease)
return lease
def log_lease (self, lease, old_lease=None) :
"""
Log given lease transition on stdout.
"""
# log
if old_lease :
log.info("%-15s: %20s @ %8s <- %-8s @ %20s", old_lease['lease'],
old_lease.get('ends', '???'),
old_lease.get('next-binding-state', ''), # optional
old_lease.get('binding-state', '???'),
old_lease.get('starts', '???'),
)
log.info("%-15s: %20s @ %8s -> %-8s @ %20s", lease['lease'],
lease.get('starts', '???'),
lease.get('binding-state', '???'),
lease.get('next-binding-state', ''), # optional
lease.get('ends', '???'),
)
def process_block (self, blockdata, log_leases=False) :
"""
Process given block (from DHCPConfigParser.parse()), to update state.
"""
block, items = blockdata
type = block.pop(0)
args = block
if type == 'lease' :
if len(args) != 1 :
return log.warn("lease block with weird args, ignore: %s", args)
# the lease address
lease, = args
log.debug("lease: %s: %s", lease, items)
if lease in self._leases :
old = self._leases[lease]
else :
old = None
new = self.process_lease(lease, items)
if log_leases :
self.log_lease(new, old)
return new
else :
log.warn("unknown block: %s: %s", type, args)
def readleases (self) :
"""
Read new lines from the leases database and update our state.
Yields changed leases. On startup and on periodic database reset, all leases are yielded.
"""
# handle file replace by reading until EOF
sync = False
# leases = []
if self._leases is None :
# initial sync
self.reset()
sync = True
# parse in any new lines from TailFile... yields None if the file was replaced
for line in self.source.readlines(eof_mark=True) :
if line is None :
log.info("Reading new dhcpd.leases")
# resync
self.reset()
sync = True
else :
# parse
for blockdata in self.parser.parse_line(line) :
# don't log if syncing, only on normal updates (next tail-cycle)
lease = self.process_block(blockdata, log_leases=(not sync))
#if not sync :
# leases.append(lease)
yield lease
# if sync :
# return True, self.leases.values()
# else :
# return False, leases
__iter__ = readleases
def leases (self) :
"""
Iterate over all leases.
"""
return self._leases.itervalues()
# utils
def lease_state (self, lease) :
"""
Get state for lease.
"""
# count by state
starts = lease.get('starts')
state = lease.get('binding-state')
next_state = lease.get('next-binding-state')
ends = lease.get('ends')
#log.debug("%-15s: %s: %8s -> %-8s: %s", ip, starts, state, next_state or '', ends)
# XXX: datetime UTC or local?
if next_state and ends and ends < datetime.now() :
state = next_state
return state
# XXX: from db.dhcp_leases instead?
import pvl.verkko.db as db
class DHCPLeasesDatabase (object) :
"""
pvl.verkko.Database dhcp_leases model for updates.
"""
def __init__ (self, db) :
"""
db - pvl.verkko.Database
"""
self.db = db
def create (self) :
"""
CREATE TABLEs
"""
log.info("Creating database tables: dhcp_leases")
db.dhcp_leases.create(self.db.engine, checkfirst=True)
def update (self, lease) :
"""
Try an extend an existing lease?
"""
c = db.dhcp_leases.c
ip = lease['lease']
mac = lease.get('hwaddr')
starts = lease['starts']
ends = lease.get('ends')
update = db.dhcp_leases.update()
# XXX: if ends is None?
if mac :
# renew lease..?
update = update.where((c.ip == ip) & (c.mac == mac) & ((starts < c.ends) | (c.ends == None)))
else :
# new state for lease..?
update = update.where((c.ip == ip) & ((starts < c.ends) | (c.ends == ends)))
update = update.values(
state = lease['binding-state'],
next = lease.get('next-binding-state'),
ends = ends,
)
if lease.get('client-hostname') :
update = update.values(hostname = lease['client-hostname'])
return self.db.update(update) > 0
def insert (self, lease) :
"""
Record a new lease.
"""
c = db.dhcp_leases.c
query = db.dhcp_leases.insert().values(
ip = lease['lease'],
mac = lease['hwaddr'],
hostname = lease.get('client-hostname'),
starts = lease['starts'],
ends = lease.get('ends'),
state = lease['binding-state'],
next = lease.get('next-binding-state'),
)
return self.db.insert(query)
def __call__ (self, lease) :
"""
Process given DHCP lease to update currently active lease, or insert a new one.
XXX: transaction? *leases?
"""
# update existing?
if self.update(lease) :
log.info("Update: %s", lease)
elif lease.get('hwaddr') :
# new
id = self.insert(lease)
log.info("Insert: %s -> %d", lease, id)
else :
# may be a free lease
log.warn("Ignored lease: %s", lease)
if __name__ == '__main__' :
import logging
logging.basicConfig()
import doctest
doctest.testmod()