1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
|
import threading
import traceback
import os
import logging
from time import sleep, time, mktime
from datetime import datetime
logger = logging.getLogger(__name__)
def strfts(ts, format='%d/%m/%Y %H:%M'):
return datetime.fromtimestamp(ts).strftime(format)
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 = {}
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
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
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)
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)
class MtimeUpdater(Updater):
def __init__(self, lease_file, *a, **kw):
self.lease_file = lease_file
self.position = 0
self.last_modified = 0
Updater.__init__(self, *a, **kw)
def file_changed(self, f):
"""Callback on changed lease file
Args:
f: Lease file. File offset can be used to skip already parsed lines.
Returns: New byte offset pointing after last parsed byte.
"""
return f.tell()
def _trigger_update(self):
self.logger.info('Lease file changed, updating')
with open(self.lease_file, 'r') as f:
f.seek(self.position)
self.position = self.file_changed(f)
def run(self):
"""Periodicaly check if file has changed
From ISC DHCPD manual:
New leases are appended to the end of the dhcpd.leases file. In
order to prevent the file from becoming arbitrarily large, from
time to time dhcpd creates a new dhcpd.leases file from its in-core
lease database. Once this file has been written to disk, the old
file is renamed dhcpd.leases~, and the new file is renamed
dhcpd.leases.
"""
while True:
try:
stat = os.stat(self.lease_file)
mtime = stat.st_mtime
size = stat.st_size
if size < self.position:
self.logger.info('leases file changed - reseting pointer')
self.position = 0
try:
# checking if DHCPD performed cleanup
# cleanup during operation seems to be currently broken
# on customs so this could never execute
purge_time = os.stat(self.lease_file + '~').st_mtime
if purge_time > self.last_modified:
self.logger.info('leases file purged - reseting pointer')
self.position = 0
except FileNotFoundError:
pass
if mtime > self.last_modified:
self._trigger_update()
self.last_modified = mtime
sleep(5.0)
except Exception as e:
self.logger.exception('Exception in updater')
sleep(10.0)
class DnsmasqUpdater(MtimeUpdater):
def file_changed(self, f):
raise NotImplementedError(
"This was not tested after adding differential update")
for line in f:
ts, hwaddr, ip, name, client_id = line.split(' ')
self.update(hwaddr, int(ts), ip, name)
return f.tell()
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
|