summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorvuko <vuko@hackerspace.pl>2020-06-13 17:03:11 +0200
committervuko <vuko@hackerspace.pl>2020-06-13 17:03:11 +0200
commit9c79dddb6fc59e3ae22c3211490a8bd68ae42f7f (patch)
treeff78b4c9f3b455880ab0ea9b033b4a13da4c491b
parent68508440d10143fe7429a5d6cc6ae31264cfecf2 (diff)
downloadcheckinator-9c79dddb6fc59e3ae22c3211490a8bd68ae42f7f.tar.gz
checkinator-9c79dddb6fc59e3ae22c3211490a8bd68ae42f7f.tar.bz2
checkinator-9c79dddb6fc59e3ae22c3211490a8bd68ae42f7f.zip
separate parsing function from updater thread
-rw-r--r--at/dhcp.py216
1 files changed, 133 insertions, 83 deletions
diff --git a/at/dhcp.py b/at/dhcp.py
index 2874e27..63eba35 100644
--- a/at/dhcp.py
+++ b/at/dhcp.py
@@ -1,9 +1,10 @@
import threading
-import traceback
import os
+import io
import logging
+from typing import Tuple, List, Optional, NamedTuple
from time import sleep, time, mktime
from datetime import datetime
@@ -12,68 +13,100 @@ logger = logging.getLogger(__name__)
def strfts(ts, format='%d/%m/%Y %H:%M'):
return datetime.fromtimestamp(ts).strftime(format)
+class DhcpLease(NamedTuple):
+ hwaddr: Optional[str]
+ atime: Optional[float]
+ ip: Optional[str]
+ name: Optional[str]
+
+
+class ActiveDevices:
+ def __init__(self):
+ self._devices = {}
+
+ def purge_stale(self, timeout):
+ now = time()
+ for device in list(self._devices.values()):
+ if now - device.atime > timeout:
+ del self._devices[device.hwaddr]
+
+ def add(self, lease: DhcpLease) -> bool:
+ if lease.atime is None:
+ lease = lease._replace(atime=time())
+ if lease.hwaddr not in self._devices or self._devices[lease.hwaddr].atime < lease.atime:
+ self._devices[lease.hwaddr] = lease
+ return True
+ return False
+
+ def update(self, devices) -> List[str]:
+ '''Add entries from another ActiveDevices instance
+
+ Args:
+ devices: list of entries to be added
+
+ Returns: list of updated enties
+ '''
+
+ updated = []
+ for device in devices._devices.values():
+ if self.add(device):
+ updated.append(device)
+ return updated
+
class Updater(threading.Thread):
def __init__(self, timeout, lease_offset=0, logger=logger, *a, **kw):
self.timeout = timeout
self.lock = threading.Lock()
self.lease_offset = lease_offset
self.logger = logger
- self.active = {}
+ self.active = ActiveDevices()
threading.Thread.__init__(self, *a, **kw)
self.daemon = True
- def purge_stale(self):
- now = time()
- for addr, (atime, ip, name) in list(self.active.items()):
- if now - atime > self.timeout:
- del self.active[addr]
-
def get_active_devices(self):
with self.lock:
- self.purge_stale()
- r = dict(self.active)
- return r
+ self.active.purge_stale(self.timeout)
+ return dict(self.active._devices)
def get_device(self, ip):
- active_devices = iter(self.get_active_devices().items())
- for hwaddr, (atime, dip, name) in active_devices:
- if ip == dip:
- return hwaddr, name
+ with self.lock:
+ active_devices = iter(self.get_active_devices().values())
+ for device in active_devices:
+ if device.ip == ip:
+ return device.hwaddr, device.name
return None, None
- def update(self, hwaddr, atime=None, ip=None, name=None):
- if atime:
- atime -= self.lease_offset
- else:
- atime = time()
- with self.lock:
- if hwaddr not in self.active or self.active[hwaddr][0] < atime:
- self.active[hwaddr] = (atime, ip, name)
+ def update(self, devices: ActiveDevices):
+ for device in devices._devices.values():
+ if device.atime is not None:
+ device = device._replace(atime = device.atime - self.lease_offset)
+ with self.lock:
+ changed = self.active.add(device)
+ if changed:
self.logger.info('updated %s with atime %s and ip %s',
- hwaddr, strfts(atime), ip)
-
-
-class CapUpdater(Updater):
- def __init__(self, cap_file, *a, **kw):
- self.cap_file = cap_file
- Updater.__init__(self, *a, **kw)
-
- def run(self):
- while True:
- try:
- with open(self.cap_file, 'r', buffering=0) as f:
- self.logger.info('Updater ready on cap file %s',
- self.cap_file)
- lines = [l.strip() for l in f.read().split('\n')]
- for hwaddr in lines:
- if hwaddr:
- self.update(hwaddr)
- self.logger.warning('Cap file %s closed, reopening',
- self.cap_file)
- except Exception as e:
- self.logger.error('Updater got an exception:\n' +
- traceback.format_exc(e))
- sleep(10.0)
+ device.hwaddr, strfts(device.atime), device.ip)
+
+#class CapUpdater(Updater):
+# def __init__(self, cap_file, *a, **kw):
+# self.cap_file = cap_file
+# Updater.__init__(self, *a, **kw)
+#
+# def run(self):
+# while True:
+# try:
+# with open(self.cap_file, 'r', buffering=0) as f:
+# self.logger.info('Updater ready on cap file %s',
+# self.cap_file)
+# lines = [l.strip() for l in f.read().split('\n')]
+# for hwaddr in lines:
+# if hwaddr:
+# self.update(hwaddr)
+# self.logger.warning('Cap file %s closed, reopening',
+# self.cap_file)
+# except Exception as e:
+# self.logger.error('Updater got an exception:\n' +
+# traceback.format_exc(e))
+# sleep(10.0)
class MtimeUpdater(Updater):
@@ -133,7 +166,7 @@ class MtimeUpdater(Updater):
self._trigger_update()
self.last_modified = mtime
sleep(5.0)
- except Exception as e:
+ except Exception:
self.logger.exception('Exception in updater')
sleep(10.0)
@@ -147,41 +180,58 @@ class DnsmasqUpdater(MtimeUpdater):
self.update(hwaddr, int(ts), ip, name)
return f.tell()
+def parse_isc_dhcpd_leases(leases_file: io.TextIOBase) -> Tuple[int, ActiveDevices]:
+ """Parse ISC dhcpd server leases file
+
+ Args:
+ leases_file: opened leases file. To skip already parsed part use seek
+ before calling.
+
+ Returns: Byte offset (as returned by tell()) of last parsed entry and
+ dictionary of parsed leases
+ """
+ leases = ActiveDevices()
+
+ ip: Optional[str] = None
+ hwaddr: Optional[str] = None
+ atime: Optional[float] = None
+ name: Optional[str] = None
+
+ lease = False
+ offset = leases_file.tell()
+ while True:
+ # using readline because iter(file) blocks file.tell usage
+ line = leases_file.readline()
+ if not line:
+ return offset, leases
+ line = line.split('#')[0]
+ cmd = line.strip().split()
+ if not cmd:
+ continue
+ if lease:
+ field = cmd[0]
+ if(field == 'starts'):
+ dt = datetime.strptime(' '.join(cmd[2:]),
+ '%Y/%m/%d %H:%M:%S;')
+ atime = mktime(dt.utctimetuple())
+ if(field == 'client-hostname'):
+ name = cmd[1][1:-2]
+ if(field == 'hardware'):
+ hwaddr = cmd[2][:-1]
+ if(field.startswith('}')):
+ offset = leases_file.tell()
+ lease = False
+ if hwaddr is not None and atime is not None:
+ leases.add(DhcpLease(hwaddr, atime, ip, name))
+ hwaddr, atime = None, None
+ elif cmd[0] == 'lease':
+ ip = cmd[1]
+ hwaddr, atime, name = None, None, None
+ lease = True
+
class DhcpdUpdater(MtimeUpdater):
def file_changed(self, f):
- lease = False
- # for use by next-line logic
- ip = None
- hwaddr = None
- atime = None
- offset = f.tell()
- while True:
- # using readline because iter(file) blocks file.tell usage
- line = f.readline()
- if not line:
- return offset
- line = line.split('#')[0]
- cmd = line.strip().split()
- if not cmd:
- continue
- if lease:
- field = cmd[0]
- if(field == 'starts'):
- dt = datetime.strptime(' '.join(cmd[2:]),
- '%Y/%m/%d %H:%M:%S;')
- atime = mktime(dt.utctimetuple())
- if(field == 'client-hostname'):
- name = cmd[1][1:-2]
- if(field == 'hardware'):
- hwaddr = cmd[2][:-1]
- if(field.startswith('}')):
- offset = f.tell()
- lease = False
- if hwaddr is not None and atime is not None:
- self.update(hwaddr, atime, ip, name)
- hwaddr, atime = None, None
- elif cmd[0] == 'lease':
- ip = cmd[1]
- name, hwaddr, atime = [None] * 3
- lease = True
+ offset, devices = parse_isc_dhcpd_leases(f)
+ self.update(devices)
+ return offset