38 # options |
39 # options |
39 parser.add_option_group(pvl.args.parser(parser)) |
40 parser.add_option_group(pvl.args.parser(parser)) |
40 |
41 |
41 ## syslog |
42 ## syslog |
42 parser.add_option_group(pvl.syslog.args.parser(parser, prog=DHCP_SYSLOG_PROG)) |
43 parser.add_option_group(pvl.syslog.args.parser(parser, prog=DHCP_SYSLOG_PROG)) |
|
44 |
|
45 ## leases |
|
46 parser.add_option('--leases-file', metavar='FILE', |
|
47 help="Synchronize dhcpd leases from given file") |
|
48 |
|
49 parser.add_option('--leases-tail', type='float', metavar='POLL', |
|
50 help="Continuously poll leases file XXX: overrides --syslog-tail") |
|
51 |
43 |
52 |
44 ## XXX: networks |
53 ## XXX: networks |
45 parser.add_option('--network', metavar='NET', action='append', |
54 parser.add_option('--network', metavar='NET', action='append', |
46 help="Filter leases by network prefix as plugin instance") |
55 help="Filter leases by network prefix as plugin instance") |
47 |
56 |
69 if not options.database : |
78 if not options.database : |
70 parser.error("Missing required option: --database") |
79 parser.error("Missing required option: --database") |
71 |
80 |
72 return options, args |
81 return options, args |
73 |
82 |
74 class DHCPHostsDatabase (db.Database) : |
83 class DHCPHostsDatabase (object) : |
|
84 """ |
|
85 The dhcp_hosts table in our database. |
|
86 """ |
|
87 |
|
88 def __init__ (self, db) : |
|
89 self.db = db |
75 |
90 |
76 def create (self) : |
91 def create (self) : |
77 """ |
92 """ |
78 CREATE TABLEs |
93 CREATE TABLEs |
79 """ |
94 """ |
80 |
95 |
81 log.info("Creating database tables: dhcp_hosts") |
96 log.info("Creating database tables: dhcp_hosts") |
82 db.dhcp_hosts.create(self.engine) |
97 db.dhcp_hosts.create(self.db.engine) |
83 |
98 |
84 def insert (self, attrs) : |
99 def insert (self, attrs) : |
85 """ |
100 """ |
86 INSERT new host |
101 INSERT new host |
87 """ |
102 """ |
98 state = attrs['state'], |
113 state = attrs['state'], |
99 |
114 |
100 name = attrs.get('name'), |
115 name = attrs.get('name'), |
101 error = attrs.get('error'), |
116 error = attrs.get('error'), |
102 ) |
117 ) |
103 result = self.engine.execute(query) |
118 |
104 id, = result.inserted_primary_key |
119 # -> id |
105 |
120 return self.db.insert(query) |
106 return id |
|
107 |
121 |
108 def update (self, attrs) : |
122 def update (self, attrs) : |
109 """ |
123 """ |
110 UPDATE existing host, or return False if not found. |
124 UPDATE existing host, or return False if not found. |
111 """ |
125 """ |
126 query = query.values(name = attrs['name']) |
140 query = query.values(name = attrs['name']) |
127 |
141 |
128 if 'error' in attrs : |
142 if 'error' in attrs : |
129 query = query.values(error = attrs['error']) |
143 query = query.values(error = attrs['error']) |
130 |
144 |
131 result = self.engine.execute(query) |
|
132 |
|
133 # any matched rows? |
145 # any matched rows? |
134 return result.rowcount > 0 |
146 return self.db.update(query) |
135 |
147 |
136 def process (self, item) : |
148 def process (self, item) : |
137 """ |
149 """ |
138 Process given DHCP syslog message to update the hosts table. |
150 Process given DHCP syslog message to update the hosts table. |
139 """ |
151 """ |
177 else : |
189 else : |
178 # new |
190 # new |
179 log.info("Insert: %s", attrs) |
191 log.info("Insert: %s", attrs) |
180 self.insert(attrs) |
192 self.insert(attrs) |
181 |
193 |
182 class DHCPSyslogHandler (object) : |
194 class DHCPLeasesDatabase (object) : |
183 """ |
|
184 Process lines from syslog |
|
185 """ |
|
186 |
|
187 def __init__ (self, db) : |
195 def __init__ (self, db) : |
188 self.db = db |
196 self.db = db |
189 |
197 |
|
198 def create (self) : |
|
199 """ |
|
200 CREATE TABLEs |
|
201 """ |
|
202 |
|
203 log.info("Creating database tables: dhcp_leases") |
|
204 db.dhcp_leases.create(self.db.engine) |
|
205 |
|
206 def update (self, lease) : |
|
207 """ |
|
208 Try an extend an existing lease? |
|
209 """ |
|
210 |
|
211 c = db.dhcp_leases.c |
|
212 |
|
213 ip = lease['lease'] |
|
214 mac = lease.get('hwaddr') |
|
215 starts = lease['starts'] |
|
216 ends = lease['ends'] |
|
217 |
|
218 update = db.dhcp_leases.update() |
|
219 |
|
220 if mac : |
|
221 # renew lease..? |
|
222 update = update.where((c.ip == ip) & (c.mac == mac) & ((starts < c.ends) | (c.ends == None))) |
|
223 else : |
|
224 # new state for lease..? |
|
225 update = update.where((c.ip == ip) & ((starts < c.ends) | (c.ends == ends))) |
|
226 |
|
227 update = update.values( |
|
228 state = lease['binding-state'], |
|
229 next = lease.get('next-binding-state'), |
|
230 ends = lease['ends'], |
|
231 ) |
|
232 |
|
233 if lease.get('client-hostname') : |
|
234 update = update.values(hostname = lease['client-hostname']) |
|
235 |
|
236 return self.db.update(update) > 0 |
|
237 |
|
238 def insert (self, lease) : |
|
239 """ |
|
240 Record a new lease. |
|
241 """ |
|
242 |
|
243 c = db.dhcp_leases.c |
|
244 |
|
245 query = db.dhcp_leases.insert().values( |
|
246 ip = lease['lease'], |
|
247 mac = lease['hwaddr'], |
|
248 hostname = lease.get('client-hostname'), |
|
249 |
|
250 starts = lease['starts'], |
|
251 ends = lease['ends'], |
|
252 |
|
253 state = lease['binding-state'], |
|
254 next = lease.get('next-binding-state'), |
|
255 ) |
|
256 |
|
257 return self.db.insert(query) |
|
258 |
|
259 def process (self, lease) : |
|
260 """ |
|
261 Process given DHCP lease to update currently active lease, or insert a new one. |
|
262 """ |
|
263 |
|
264 # update existing? |
|
265 if self.update(lease) : |
|
266 log.info("Update: %s", lease) |
|
267 |
|
268 elif lease.get('hwaddr') : |
|
269 # new |
|
270 id = self.insert(lease) |
|
271 |
|
272 log.info("Insert: %s -> %d", lease, id) |
|
273 |
|
274 else : |
|
275 # may be a free lease |
|
276 log.warn("Ignored lease: %s", lease) |
|
277 |
|
278 class DHCPHandler (object) : |
|
279 """ |
|
280 Process lines from syslog |
|
281 """ |
|
282 |
|
283 def __init__ (self, db, syslog, leases) : |
|
284 self.db = db |
|
285 |
|
286 self.syslog = syslog |
|
287 self.leases = leases |
|
288 |
|
289 self.hostdb = DHCPHostsDatabase(db) |
|
290 self.leasedb = DHCPLeasesDatabase(db) |
|
291 |
190 # XXX |
292 # XXX |
191 self.filter = pvl.syslog.dhcp.DHCPSyslogFilter() |
293 self.filter = pvl.syslog.dhcp.DHCPSyslogFilter() |
192 |
294 |
193 def process (self, item) : |
295 def createdb (self) : |
|
296 """ |
|
297 Initialize database tables. |
|
298 """ |
|
299 |
|
300 self.hostdb.create() |
|
301 self.leasedb.create() |
|
302 |
|
303 def process_syslog (self, item) : |
194 """ |
304 """ |
195 Handle a single item read from syslog to DB. |
305 Handle a single item read from syslog to DB. |
196 filter = pvl.syslog.dhcp.DHCPSyslogFilter() |
|
197 """ |
306 """ |
198 |
307 |
199 dhcp_item = self.filter.parse(item['msg']) |
308 dhcp_item = self.filter.parse(item['msg']) |
200 |
309 |
201 if not dhcp_item : |
310 if not dhcp_item : |
205 dhcp_item['timestamp'] = item['timestamp'] # XXX: fixup DHCPSyslogParser? |
314 dhcp_item['timestamp'] = item['timestamp'] # XXX: fixup DHCPSyslogParser? |
206 |
315 |
207 if item.get('error') : |
316 if item.get('error') : |
208 item['error'] = self.filter.parse_error(item['error']) |
317 item['error'] = self.filter.parse_error(item['error']) |
209 |
318 |
210 self.db.process(dhcp_item) |
319 self.hostdb.process(dhcp_item) |
211 |
320 |
212 def main (self, source, poll) : |
321 def process_leases (self, leases) : |
|
322 """ |
|
323 Handle given changed lease. |
|
324 """ |
|
325 |
|
326 for lease in leases : |
|
327 log.info("%s", lease['lease']) |
|
328 |
|
329 self.leasedb.process(lease) |
|
330 |
|
331 def sync_leases (self, leases) : |
|
332 """ |
|
333 Sync the entire given set of valid leases. |
|
334 """ |
|
335 |
|
336 log.info("%d leases", len(leases)) |
|
337 |
|
338 # XXX: any difference? Mostly removed leases, but... they should expire by themselves, right? |
|
339 self.process_leases(leases) |
|
340 |
|
341 def main (self, poll) : |
213 """ |
342 """ |
214 Read items from syslog source with given polling style. |
343 Read items from syslog source with given polling style. |
215 |
344 |
216 TODO: poll = false on SIGINT |
345 TODO: poll = false on SIGINT |
217 """ |
346 """ |
218 |
347 |
219 parser = pvl.syslog.parser.SyslogParser() |
348 parser = pvl.syslog.parser.SyslogParser() |
220 |
349 |
221 # mainloop |
350 # mainloop |
222 while True : |
351 while True : |
223 # process from source |
352 if self.syslog : |
224 for item in parser.process(source) : |
353 # process from source |
225 self.process(item) |
354 for item in parser.process(self.syslog) : |
|
355 self.process_syslog(item) |
226 |
356 |
|
357 if self.leases : |
|
358 # process internally |
|
359 #sync, leases = self.leases.process() |
|
360 leases = self.leases.process() |
|
361 |
|
362 #if sync : |
|
363 # self.sync_leases(leases) |
|
364 if leases : |
|
365 self.process_leases(leases) |
|
366 #else : |
|
367 # pass |
|
368 |
227 if poll is False : |
369 if poll is False : |
228 # done |
370 # done |
229 break |
371 break |
230 else : |
372 else : |
231 # wait |
373 # wait |
240 if not options.database : |
382 if not options.database : |
241 log.error("No database given") |
383 log.error("No database given") |
242 return 1 |
384 return 1 |
243 |
385 |
244 log.info("Open up database: %s", options.database) |
386 log.info("Open up database: %s", options.database) |
245 database = DHCPHostsDatabase(options.database) |
387 database = pvl.verkko.db.Database(options.database) |
246 |
388 |
247 if options.create : |
|
248 database.create() |
|
249 |
|
250 # syslog |
389 # syslog |
251 log.info("Open up syslog...") |
390 log.info("Open up syslog...") |
252 syslog_parser = pvl.syslog.parser.SyslogParser(prog=DHCP_SYSLOG_PROG) # filter by prog |
391 syslog_parser = pvl.syslog.parser.SyslogParser(prog=DHCP_SYSLOG_PROG) # filter by prog |
253 |
392 |
254 if options.syslog_fifo : |
393 if options.syslog_fifo : |
255 source = pvl.syslog.fifo.Fifo(options.syslog_fifo) |
394 syslog = pvl.syslog.fifo.Fifo(options.syslog_fifo) |
256 poll = None # no timeout |
395 poll = None # no timeout |
257 |
396 |
258 elif options.syslog_tail : |
397 elif options.syslog_tail : |
259 # continuous file tail |
398 # continuous file tail |
260 source = pvl.syslog.tail.TailFile(options.syslog_file) |
399 syslog = pvl.syslog.tail.TailFile(options.syslog_file) |
261 poll = options.syslog_tail # polling interval |
400 poll = options.syslog_tail # polling interval |
262 |
401 |
263 elif options.syslog_file : |
402 elif options.syslog_file : |
264 # one-shot file read |
403 # one-shot file read |
265 source = pvl.syslog.tail.TailFile(options.syslog_file) |
404 syslog = pvl.syslog.tail.TailFile(options.syslog_file) |
266 poll = False # do not poll-loop |
405 poll = False # do not poll-loop |
267 |
406 |
268 else : |
407 else : |
|
408 syslog = None |
|
409 poll = False |
269 log.warning("No syslog source given") |
410 log.warning("No syslog source given") |
270 return 0 |
411 |
|
412 # leases |
|
413 if options.leases_file : |
|
414 log.info("Open up DHCP leases...") |
|
415 leases = pvl.verkko.dhcp.leases.DHCPLeasesDatabase(options.leases_file) |
|
416 |
|
417 # force polling interval |
|
418 if options.leases_tail : |
|
419 # XXX: min between syslog/leases? |
|
420 poll = options.leases_tail |
|
421 |
|
422 else : |
|
423 leases = None |
271 |
424 |
272 # handler + main |
425 # handler + main |
273 handler = DHCPSyslogHandler(database) |
426 handler = DHCPHandler(database, syslog, leases) |
274 |
427 |
|
428 if options.create : |
|
429 handler.createdb() |
|
430 |
275 log.info("Enter mainloop...") |
431 log.info("Enter mainloop...") |
276 handler.main(source, poll) |
432 handler.main(poll) |
277 |
433 |
278 # done |
434 # done |
279 return 0 |
435 return 0 |
280 |
436 |
281 if __name__ == '__main__': |
437 if __name__ == '__main__': |