David K. Bainbridge | b541504 | 2016-05-13 17:06:10 -0700 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | import sys, threading, thread, subprocess, re, time, datetime, bisect, BaseHTTPServer |
| 3 | from optparse import OptionParser |
| 4 | from Queue import Queue |
| 5 | |
| 6 | def parse_timestamp(raw_str): |
| 7 | tokens = raw_str.split() |
| 8 | |
| 9 | if len(tokens) == 1: |
| 10 | if tokens[0].lower() == 'never': |
| 11 | return 'never'; |
| 12 | |
| 13 | else: |
| 14 | raise Exception('Parse error in timestamp') |
| 15 | |
| 16 | elif len(tokens) == 3: |
| 17 | return datetime.datetime.strptime(' '.join(tokens[1:]), |
| 18 | '%Y/%m/%d %H:%M:%S') |
| 19 | |
| 20 | else: |
| 21 | raise Exception('Parse error in timestamp') |
| 22 | |
| 23 | def timestamp_is_ge(t1, t2): |
| 24 | if t1 == 'never': |
| 25 | return True |
| 26 | |
| 27 | elif t2 == 'never': |
| 28 | return False |
| 29 | |
| 30 | else: |
| 31 | return t1 >= t2 |
| 32 | |
| 33 | |
| 34 | def timestamp_is_lt(t1, t2): |
| 35 | if t1 == 'never': |
| 36 | return False |
| 37 | |
| 38 | elif t2 == 'never': |
| 39 | return t1 != 'never' |
| 40 | |
| 41 | else: |
| 42 | return t1 < t2 |
| 43 | |
| 44 | |
| 45 | def timestamp_is_between(t, tstart, tend): |
| 46 | return timestamp_is_ge(t, tstart) and timestamp_is_lt(t, tend) |
| 47 | |
| 48 | |
| 49 | def parse_hardware(raw_str): |
| 50 | tokens = raw_str.split() |
| 51 | |
| 52 | if len(tokens) == 2: |
| 53 | return tokens[1] |
| 54 | |
| 55 | else: |
| 56 | raise Exception('Parse error in hardware') |
| 57 | |
| 58 | |
| 59 | def strip_endquotes(raw_str): |
| 60 | return raw_str.strip('"') |
| 61 | |
| 62 | |
| 63 | def identity(raw_str): |
| 64 | return raw_str |
| 65 | |
| 66 | |
| 67 | def parse_binding_state(raw_str): |
| 68 | tokens = raw_str.split() |
| 69 | |
| 70 | if len(tokens) == 2: |
| 71 | return tokens[1] |
| 72 | |
| 73 | else: |
| 74 | raise Exception('Parse error in binding state') |
| 75 | |
| 76 | |
| 77 | def parse_next_binding_state(raw_str): |
| 78 | tokens = raw_str.split() |
| 79 | |
| 80 | if len(tokens) == 3: |
| 81 | return tokens[2] |
| 82 | |
| 83 | else: |
| 84 | raise Exception('Parse error in next binding state') |
| 85 | |
| 86 | |
| 87 | def parse_rewind_binding_state(raw_str): |
| 88 | tokens = raw_str.split() |
| 89 | |
| 90 | if len(tokens) == 3: |
| 91 | return tokens[2] |
| 92 | |
| 93 | else: |
| 94 | raise Exception('Parse error in next binding state') |
| 95 | |
| 96 | def parse_res_fixed_address(raw_str): |
| 97 | return raw_str |
| 98 | |
| 99 | def parse_res_hardware(raw_str): |
| 100 | tokens = raw_str.split() |
| 101 | return tokens[1] |
| 102 | |
| 103 | def parse_reservation_file(res_file): |
| 104 | valid_keys = { |
| 105 | 'hardware' : parse_res_hardware, |
| 106 | 'fixed-address' : parse_res_fixed_address, |
| 107 | } |
| 108 | |
| 109 | res_db = {} |
| 110 | res_rec = {} |
| 111 | in_res = False |
| 112 | for line in res_file: |
| 113 | if line.lstrip().startswith('#'): |
| 114 | continue |
| 115 | tokens = line.split() |
| 116 | |
| 117 | if len(tokens) == 0: |
| 118 | continue |
| 119 | |
| 120 | key = tokens[0].lower() |
| 121 | |
| 122 | if key == 'host': |
| 123 | if not in_res: |
| 124 | res_rec = {'hostname' : tokens[1]} |
| 125 | in_res = True |
| 126 | |
| 127 | else: |
| 128 | raise Exception("Parse error in reservation file") |
| 129 | elif key == '}': |
| 130 | if in_res: |
| 131 | for k in valid_keys: |
| 132 | if callable(valid_keys[k]): |
| 133 | res_rec[k] = res_rec.get(k, '') |
| 134 | else: |
| 135 | res_rec[k] = False |
| 136 | |
| 137 | hostname = res_rec['hostname'] |
| 138 | |
| 139 | if hostname in res_db: |
| 140 | res_db[hostname].insert(0, res_rec) |
| 141 | |
| 142 | else: |
| 143 | res_db[hostname] = [res_rec] |
| 144 | |
| 145 | res_rec = {} |
| 146 | in_res = False |
| 147 | |
| 148 | else: |
| 149 | raise Exception('Parse error in reservation file') |
| 150 | |
| 151 | elif key in valid_keys: |
| 152 | if in_res: |
| 153 | value = line[(line.index(key) + len(key)):] |
| 154 | value = value.strip().rstrip(';').rstrip() |
| 155 | |
| 156 | if callable(valid_keys[key]): |
| 157 | res_rec[key] = valid_keys[key](value) |
| 158 | else: |
| 159 | res_rec[key] = True |
| 160 | |
| 161 | else: |
| 162 | raise Exception('Parse error in reservation file') |
| 163 | |
| 164 | else: |
| 165 | if in_res: |
| 166 | raise Exception('Parse error in reservation file') |
| 167 | |
| 168 | if in_res: |
| 169 | raise Exception('Parse error in reservation file') |
| 170 | |
| 171 | # Turn the leases into an array |
| 172 | results = [] |
| 173 | for res in res_db: |
| 174 | results.append({ |
| 175 | 'client-hostname' : res_db[res][0]['hostname'], |
| 176 | 'hardware' : res_db[res][0]['hardware'], |
| 177 | 'ip_address' : res_db[res][0]['fixed-address'], |
| 178 | }) |
| 179 | return results |
| 180 | |
| 181 | |
| 182 | def parse_leases_file(leases_file): |
| 183 | valid_keys = { |
| 184 | 'starts': parse_timestamp, |
| 185 | 'ends': parse_timestamp, |
| 186 | 'tstp': parse_timestamp, |
| 187 | 'tsfp': parse_timestamp, |
| 188 | 'atsfp': parse_timestamp, |
| 189 | 'cltt': parse_timestamp, |
| 190 | 'hardware': parse_hardware, |
| 191 | 'binding': parse_binding_state, |
| 192 | 'next': parse_next_binding_state, |
| 193 | 'rewind': parse_rewind_binding_state, |
| 194 | 'uid': strip_endquotes, |
| 195 | 'client-hostname': strip_endquotes, |
| 196 | 'option': identity, |
| 197 | 'set': identity, |
| 198 | 'on': identity, |
| 199 | 'abandoned': None, |
| 200 | 'bootp': None, |
| 201 | 'reserved': None, |
| 202 | } |
| 203 | |
| 204 | leases_db = {} |
| 205 | |
| 206 | lease_rec = {} |
| 207 | in_lease = False |
| 208 | in_failover = False |
| 209 | |
| 210 | for line in leases_file: |
| 211 | if line.lstrip().startswith('#'): |
| 212 | continue |
| 213 | |
| 214 | tokens = line.split() |
| 215 | |
| 216 | if len(tokens) == 0: |
| 217 | continue |
| 218 | |
| 219 | key = tokens[0].lower() |
| 220 | |
| 221 | if key == 'lease': |
| 222 | if not in_lease: |
| 223 | ip_address = tokens[1] |
| 224 | |
| 225 | lease_rec = {'ip_address' : ip_address} |
| 226 | in_lease = True |
| 227 | |
| 228 | else: |
| 229 | raise Exception('Parse error in leases file') |
| 230 | |
| 231 | elif key == 'failover': |
| 232 | in_failover = True |
| 233 | elif key == '}': |
| 234 | if in_lease: |
| 235 | for k in valid_keys: |
| 236 | if callable(valid_keys[k]): |
| 237 | lease_rec[k] = lease_rec.get(k, '') |
| 238 | else: |
| 239 | lease_rec[k] = False |
| 240 | |
| 241 | ip_address = lease_rec['ip_address'] |
| 242 | |
| 243 | if ip_address in leases_db: |
| 244 | leases_db[ip_address].insert(0, lease_rec) |
| 245 | |
| 246 | else: |
| 247 | leases_db[ip_address] = [lease_rec] |
| 248 | |
| 249 | lease_rec = {} |
| 250 | in_lease = False |
| 251 | |
| 252 | elif in_failover: |
| 253 | in_failover = False |
| 254 | continue |
| 255 | else: |
| 256 | raise Exception('Parse error in leases file') |
| 257 | |
| 258 | elif key in valid_keys: |
| 259 | if in_lease: |
| 260 | value = line[(line.index(key) + len(key)):] |
| 261 | value = value.strip().rstrip(';').rstrip() |
| 262 | |
| 263 | if callable(valid_keys[key]): |
| 264 | lease_rec[key] = valid_keys[key](value) |
| 265 | else: |
| 266 | lease_rec[key] = True |
| 267 | |
| 268 | else: |
| 269 | raise Exception('Parse error in leases file') |
| 270 | |
| 271 | else: |
| 272 | if in_lease: |
| 273 | raise Exception('Parse error in leases file') |
| 274 | |
| 275 | if in_lease: |
| 276 | raise Exception('Parse error in leases file') |
| 277 | |
| 278 | return leases_db |
| 279 | |
| 280 | |
| 281 | def round_timedelta(tdelta): |
| 282 | return datetime.timedelta(tdelta.days, |
| 283 | tdelta.seconds + (0 if tdelta.microseconds < 500000 else 1)) |
| 284 | |
| 285 | |
| 286 | def timestamp_now(): |
| 287 | n = datetime.datetime.utcnow() |
| 288 | return datetime.datetime(n.year, n.month, n.day, n.hour, n.minute, |
| 289 | n.second)# + (0 if n.microsecond < 500000 else 1)) |
| 290 | |
| 291 | |
| 292 | def lease_is_active(lease_rec, as_of_ts): |
| 293 | return lease_rec['binding'] != 'free' and timestamp_is_between(as_of_ts, lease_rec['starts'], |
| 294 | lease_rec['ends']) |
| 295 | |
| 296 | |
| 297 | def ipv4_to_int(ipv4_addr): |
| 298 | parts = ipv4_addr.split('.') |
| 299 | return (int(parts[0]) << 24) + (int(parts[1]) << 16) + \ |
| 300 | (int(parts[2]) << 8) + int(parts[3]) |
| 301 | |
| 302 | def select_active_leases(leases_db, as_of_ts): |
| 303 | retarray = [] |
| 304 | sortedarray = [] |
| 305 | |
| 306 | for ip_address in leases_db: |
| 307 | lease_rec = leases_db[ip_address][0] |
| 308 | |
| 309 | if lease_is_active(lease_rec, as_of_ts): |
| 310 | ip_as_int = ipv4_to_int(ip_address) |
| 311 | insertpos = bisect.bisect(sortedarray, ip_as_int) |
| 312 | sortedarray.insert(insertpos, ip_as_int) |
| 313 | retarray.insert(insertpos, lease_rec) |
| 314 | |
| 315 | return retarray |
| 316 | |
| 317 | def matched(list, target): |
| 318 | if list == None: |
| 319 | return False |
| 320 | |
| 321 | for r in list: |
| 322 | if re.match(r, target) != None: |
| 323 | return True |
| 324 | return False |
| 325 | |
| 326 | def convert_to_seconds(time_val): |
| 327 | num = int(time_val[:-1]) |
| 328 | if time_val.endswith('s'): |
| 329 | return num |
| 330 | elif time_val.endswith('m'): |
| 331 | return num * 60 |
| 332 | elif time_val.endswith('h'): |
| 333 | return num * 60 * 60 |
| 334 | elif time_val.endswith('d'): |
| 335 | return num * 60 * 60 * 24 |
| 336 | |
| 337 | def ping(ip, timeout): |
| 338 | cmd = ['ping', '-c', '1', '-w', timeout, ip] |
| 339 | try: |
| 340 | out = subprocess.check_output(cmd) |
| 341 | return True |
| 342 | except subprocess.CalledProcessError as e: |
| 343 | return False |
| 344 | |
| 345 | def ping_worker(list, to, respQ): |
| 346 | for lease in list: |
| 347 | respQ.put( |
| 348 | { |
| 349 | 'verified': ping(lease['ip_address'], to), |
| 350 | 'lease' : lease, |
| 351 | }) |
| 352 | |
| 353 | def interruptable_get(q): |
| 354 | r = None |
| 355 | while True: |
| 356 | try: |
| 357 | return q.get(timeout=1000) |
| 358 | except Queue.Empty: |
| 359 | pass |
| 360 | |
| 361 | ############################################################################## |
| 362 | |
| 363 | def harvest(options): |
| 364 | |
| 365 | ifilter = None |
| 366 | if options.include != None: |
| 367 | ifilter = options.include.translate(None, ' ').split(',') |
| 368 | |
| 369 | rfilter = None |
| 370 | if options.filter != None: |
| 371 | rfilter = options.filter.split(',') |
| 372 | |
| 373 | myfile = open(options.leases, 'r') |
| 374 | leases = parse_leases_file(myfile) |
| 375 | myfile.close() |
| 376 | |
| 377 | reservations = [] |
| 378 | try: |
| 379 | with open(options.reservations, 'r') as res_file: |
| 380 | reservations = parse_reservation_file(res_file) |
| 381 | res_file.close() |
| 382 | except (IOError) as e: |
| 383 | pass |
| 384 | |
| 385 | now = timestamp_now() |
| 386 | report_dataset = select_active_leases(leases, now) + reservations |
| 387 | |
| 388 | verified = [] |
| 389 | if options.verify: |
| 390 | |
| 391 | # To verify is lease information is valid, i.e. that the host which got the lease still responding |
| 392 | # we ping the host. Not perfect, but good for the main use case. As the lease file can get long |
| 393 | # a little concurrency is used. The lease list is divided amoung workers and each worker takes |
| 394 | # a share. |
| 395 | respQ = Queue() |
| 396 | to = str(convert_to_seconds(options.timeout)) |
| 397 | share = int(len(report_dataset) / options.worker_count) |
| 398 | extra = len(report_dataset) % options.worker_count |
| 399 | start = 0 |
| 400 | for idx in range(0, options.worker_count): |
| 401 | end = start + share |
| 402 | if extra > 0: |
| 403 | end = end + 1 |
| 404 | extra = extra - 1 |
| 405 | worker = threading.Thread(target=ping_worker, args=(report_dataset[start:end], to, respQ)) |
| 406 | worker.daemon = True |
| 407 | worker.start() |
| 408 | start = end |
| 409 | |
| 410 | # All the verification work has been farmed out to worker threads, so sit back and wait for reponses. |
| 411 | # Once all responses are received we are done. Probably should put a time out here as well, but for |
| 412 | # now we expect a response for every lease, either positive or negative |
| 413 | count = 0 |
| 414 | while count != len(report_dataset): |
| 415 | resp = interruptable_get(respQ) |
| 416 | count = count + 1 |
| 417 | if resp['verified']: |
| 418 | print("INFO: verified host '%s' with address '%s'" % (resp['lease']['client-hostname'], resp['lease']['ip_address'])) |
| 419 | verified.append(resp['lease']) |
| 420 | else: |
| 421 | print("INFO: dropping host '%s' with address '%s' (not verified)" % (resp['lease']['client-hostname'], resp['lease']['ip_address'])) |
| 422 | else: |
| 423 | verified = report_dataset |
| 424 | |
| 425 | # Look for duplicate names and add the compressed MAC as a suffix |
| 426 | names = {} |
| 427 | for lease in verified: |
| 428 | # If no client hostname use MAC |
| 429 | name = lease['client-hostname'] |
| 430 | if 'client-hostname' not in lease or len(name) == 0: |
| 431 | name = "UNK-" + lease['hardware'].translate(None, ':').upper() |
| 432 | |
| 433 | if name in names: |
| 434 | names[name] = '+' |
| 435 | else: |
| 436 | names[name] = '-' |
| 437 | |
| 438 | size = 0 |
| 439 | count = 0 |
| 440 | for lease in verified: |
| 441 | name = lease['client-hostname'] |
| 442 | if 'client-hostname' not in lease or len(name) == 0: |
| 443 | name = "UNK-" + lease['hardware'].translate(None, ':').upper() |
| 444 | |
| 445 | if (ifilter != None and name in ifilter) or matched(rfilter, name): |
| 446 | if names[name] == '+': |
| 447 | lease['client-hostname'] = name + '-' + lease['hardware'].translate(None, ':').upper() |
| 448 | size = max(size, len(lease['client-hostname'])) |
| 449 | count += 1 |
| 450 | |
| 451 | if options.dest == '-': |
| 452 | out=sys.stdout |
| 453 | else: |
| 454 | out=open(options.dest, 'w+') |
| 455 | |
| 456 | for lease in verified: |
| 457 | name = lease['client-hostname'] |
| 458 | if 'client-hostname' not in lease or len(name) == 0: |
| 459 | name = "UNK-" + lease['hardware'].translate(None, ':').upper() |
| 460 | |
| 461 | if ifilter != None and name in ifilter or matched(rfilter, name): |
| 462 | out.write(format(name, '<'+str(size)) + ' IN A ' + lease['ip_address'] + '\n') |
| 463 | if options.dest != '-': |
| 464 | out.close() |
| 465 | return count |
| 466 | |
| 467 | def reload_zone(rndc, server, port, key, zone): |
| 468 | cmd = [rndc, '-s', server] |
| 469 | if key != None: |
| 470 | cmd.extend(['-c', key]) |
| 471 | cmd.extend(['-p', port, 'reload']) |
| 472 | if zone != None: |
| 473 | cmd.append(zone) |
| 474 | |
| 475 | try: |
| 476 | out = subprocess.check_output(cmd) |
| 477 | print("INFO: [%s UTC] updated DNS sever" % time.asctime(time.gmtime())) |
| 478 | except subprocess.CalledProcessError as e: |
| 479 | print("ERROR: failed to update DNS server, exit code %d" % e.returncode) |
| 480 | print(e.output) |
| 481 | |
| 482 | def handleRequestsUsing(requestQ): |
| 483 | return lambda *args: ApiHandler(requestQ, *args) |
| 484 | |
| 485 | class ApiHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
| 486 | def __init__(s, requestQ, *args): |
| 487 | s.requestQ = requestQ |
| 488 | BaseHTTPServer.BaseHTTPRequestHandler.__init__(s, *args) |
| 489 | |
| 490 | def do_HEAD(s): |
| 491 | s.send_response(200) |
| 492 | s.send_header("Content-type", "application/json") |
| 493 | s.end_headers() |
| 494 | |
| 495 | def do_POST(s): |
| 496 | if s.path == '/harvest': |
| 497 | waitQ = Queue() |
| 498 | s.requestQ.put(waitQ) |
| 499 | resp = waitQ.get(block=True, timeout=None) |
| 500 | s.send_response(200) |
| 501 | s.send_header('Content-type', 'application/json') |
| 502 | s.end_headers() |
| 503 | |
| 504 | if resp == "QUIET": |
| 505 | s.wfile.write('{ "response" : "QUIET" }') |
| 506 | else: |
| 507 | s.wfile.write('{ "response" : "OK" }') |
| 508 | |
| 509 | else: |
| 510 | s.send_response(404) |
| 511 | |
| 512 | def do_GET(s): |
| 513 | """Respond to a GET request.""" |
| 514 | s.send_response(404) |
| 515 | |
| 516 | def do_api(hostname, port, requestQ): |
| 517 | server_class = BaseHTTPServer.HTTPServer |
| 518 | httpd = server_class((hostname, int(port)), handleRequestsUsing(requestQ)) |
| 519 | print("INFO: [%s UTC] Start API server on %s:%s" % (time.asctime(time.gmtime()), hostname, port)) |
| 520 | try: |
| 521 | httpd.serve_forever() |
| 522 | except KeyboardInterrupt: |
| 523 | pass |
| 524 | httpd.server_close() |
| 525 | print("INFO: [%s UTC] Stop API server on %s:%s" % (time.asctime(time.gmtime()), hostname, port)) |
| 526 | |
| 527 | def harvester(options, requestQ): |
| 528 | quiet = convert_to_seconds(options.quiet) |
| 529 | last = -1 |
| 530 | resp = "OK" |
| 531 | while True: |
| 532 | responseQ = requestQ.get(block=True, timeout=None) |
| 533 | if last == -1 or (time.time() - last) > quiet: |
| 534 | work_field(options) |
| 535 | last = time.time() |
| 536 | resp = "OK" |
| 537 | else: |
| 538 | resp = "QUIET" |
| 539 | |
| 540 | if responseQ != None: |
| 541 | responseQ.put(resp) |
| 542 | |
| 543 | def work_field(options): |
| 544 | start = datetime.datetime.now() |
| 545 | print("INFO: [%s UTC] starting to harvest hosts from DHCP" % (time.asctime(time.gmtime()))) |
| 546 | count = harvest(options) |
| 547 | end = datetime.datetime.now() |
| 548 | delta = end - start |
| 549 | print("INFO: [%s UTC] harvested %d hosts, taking %d seconds" % (time.asctime(time.gmtime()), count, delta.seconds)) |
| 550 | if options.update: |
| 551 | reload_zone(options.rndc, options.server, options.port, options.key, options.zone) |
| 552 | |
| 553 | def main(): |
| 554 | parser = OptionParser() |
| 555 | parser.add_option('-l', '--leases', dest='leases', default='/dhcp/dhcpd.leases', |
| 556 | help="specifies the DHCP lease file from which to harvest") |
| 557 | parser.add_option('-x', '--reservations', dest='reservations', default='/etc/dhcp/dhcpd.reservations', |
| 558 | help="specified the reservation file as ISC DHCP doesn't update the lease file for fixed addresses") |
| 559 | parser.add_option('-d', '--dest', dest='dest', default='/bind/dhcp_harvest.inc', |
| 560 | help="specifies the file to write the additional DNS information") |
| 561 | parser.add_option('-i', '--include', dest='include', default=None, |
| 562 | help="list of hostnames to include when harvesting DNS information") |
| 563 | parser.add_option('-f', '--filter', dest='filter', default=None, |
| 564 | help="list of regex expressions to use as an include filter") |
| 565 | parser.add_option('-r', '--repeat', dest='repeat', default=None, |
| 566 | help="continues to harvest DHCP information every specified interval") |
| 567 | parser.add_option('-c', '--command', dest='rndc', default='rndc', |
| 568 | help="shell command to execute to cause reload") |
| 569 | parser.add_option('-k', '--key', dest='key', default=None, |
| 570 | help="rndc key file to use to access DNS server") |
| 571 | parser.add_option('-s', '--server', dest='server', default='127.0.0.1', |
| 572 | help="server to reload after generating updated dns information") |
| 573 | parser.add_option('-p', '--port', dest='port', default='954', |
| 574 | help="port on server to contact to reload server") |
| 575 | parser.add_option('-z', '--zone', dest='zone', default=None, |
| 576 | help="zone to reload after generating updated dns information") |
| 577 | parser.add_option('-u', '--update', dest='update', default=False, action='store_true', |
| 578 | help="update the DNS server, by reloading the zone") |
| 579 | parser.add_option('-y', '--verify', dest='verify', default=False, action='store_true', |
| 580 | help="verify the hosts with a ping before pushing them to DNS") |
| 581 | parser.add_option('-t', '--timeout', dest='timeout', default='1s', |
| 582 | help="specifies the duration to wait for a verification ping from a host") |
| 583 | parser.add_option('-a', '--apiserver', dest='apiserver', default='0.0.0.0', |
| 584 | help="specifies the interfaces on which to listen for API requests") |
| 585 | parser.add_option('-e', '--apiport', dest='apiport', default='8954', |
| 586 | help="specifies the port on which to listen for API requests") |
| 587 | parser.add_option('-q', '--quiet', dest='quiet', default='1m', |
| 588 | help="specifieds a minimum quiet period between actually harvest times.") |
| 589 | parser.add_option('-w', '--workers', dest='worker_count', type='int', default=5, |
| 590 | help="specifies the number of workers to use when verifying IP addresses") |
| 591 | |
| 592 | (options, args) = parser.parse_args() |
| 593 | |
| 594 | # Kick off a thread to listen for HTTP requests to force a re-evaluation |
| 595 | requestQ = Queue() |
| 596 | api = threading.Thread(target=do_api, args=(options.apiserver, options.apiport, requestQ)) |
| 597 | api.daemon = True |
| 598 | api.start() |
| 599 | |
| 600 | if options.repeat == None: |
| 601 | work_field(options) |
| 602 | else: |
| 603 | secs = convert_to_seconds(options.repeat) |
| 604 | farmer = threading.Thread(target=harvester, args=(options, requestQ)) |
| 605 | farmer.daemon = True |
| 606 | farmer.start() |
| 607 | while True: |
| 608 | cropQ = Queue() |
| 609 | requestQ.put(cropQ) |
| 610 | interruptable_get(cropQ) |
| 611 | time.sleep(secs) |
| 612 | |
| 613 | if __name__ == "__main__": |
| 614 | main() |