process putatives: Introduce dbg()
[hippotat] / hippotatlib / __init__.py
CommitLineData
b0cfbfce 1# -*- python -*-
0256fc10
IJ
2#
3# Hippotat - Asinine IP Over HTTP program
4# hippotatlib/__init__.py - common library code
5#
6# Copyright 2017 Ian Jackson
7#
f85d143f 8# GPLv3+
0256fc10 9#
f85d143f
IJ
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
0256fc10 14#
f85d143f
IJ
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program, in the file GPLv3. If not,
22# see <http://www.gnu.org/licenses/>.
23
b0cfbfce 24
37ab4cdc
IJ
25import signal
26signal.signal(signal.SIGINT, signal.SIG_DFL)
27
1321ad5f 28import sys
cae50358 29import os
1321ad5f 30
b83d422a
IJ
31from zope.interface import implementer
32
040ff511
IJ
33import twisted
34from twisted.internet import reactor
1d023c89 35import twisted.internet.endpoints
8c3b6620
IJ
36import twisted.logger
37from twisted.logger import LogLevel
38import twisted.python.constants
39from twisted.python.constants import NamedConstant
b0cfbfce
IJ
40
41import ipaddress
42from ipaddress import AddressValueError
43
ae7c7784 44from optparse import OptionParser
5510890e 45import configparser
ae7c7784
IJ
46from configparser import ConfigParser
47from configparser import NoOptionError
48
c13ee6e6
IJ
49from functools import partial
50
ae7c7784 51import collections
84e763c7 52import time
8c3b6620 53import codecs
eedc8b30 54import traceback
ae7c7784 55
1321ad5f
IJ
56import re as regexp
57
5a37bac8 58import hippotatlib.slip as slip
1321ad5f 59
d579a048 60class DBG(twisted.python.constants.Names):
380ed56c 61 INIT = NamedConstant()
cae50358 62 CONFIG = NamedConstant()
d579a048 63 ROUTE = NamedConstant()
b68c0739 64 DROP = NamedConstant()
4a780703 65 OWNSOURCE = NamedConstant()
d579a048
IJ
66 FLOW = NamedConstant()
67 HTTP = NamedConstant()
380ed56c 68 TWISTED = NamedConstant()
d579a048 69 QUEUE = NamedConstant()
380ed56c 70 HTTP_CTRL = NamedConstant()
d579a048 71 QUEUE_CTRL = NamedConstant()
297b3ebf 72 HTTP_FULL = NamedConstant()
0accf0d3 73 CTRL_DUMP = NamedConstant()
380ed56c 74 SLIP_FULL = NamedConstant()
9acb0eca 75 DATA_COMPLETE = NamedConstant()
d579a048 76
b68c0739 77_hex_codec = codecs.getencoder('hex_codec')
8c3b6620 78
b83d422a
IJ
79#---------- logging ----------
80
81org_stderr = sys.stderr
82
8c3b6620
IJ
83log = twisted.logger.Logger()
84
2e68eb10
IJ
85debug_set = set()
86debug_def_detail = DBG.HTTP
3e35fc99 87
8c3b6620 88def log_debug(dflag, msg, idof=None, d=None):
3e35fc99 89 if dflag not in debug_set: return
e8fcf3b7 90 #print('---------------->',repr((dflag, msg, idof, d)), file=sys.stderr)
8c3b6620 91 if idof is not None:
e8ed0029 92 msg = '[%#x] %s' % (id(idof), msg)
8c3b6620 93 if d is not None:
9acb0eca
IJ
94 trunc = ''
95 if not DBG.DATA_COMPLETE in debug_set:
96 if len(d) > 64:
97 d = d[0:64]
98 trunc = '...'
b68c0739 99 d = _hex_codec(d)[0].decode('ascii')
9acb0eca 100 msg += ' ' + d + trunc
8c3b6620
IJ
101 log.info('{dflag} {msgcore}', dflag=dflag, msgcore=msg)
102
80e963a1
IJ
103def logevent_is_boringtwisted(event):
104 try:
105 if event.get('log_level') != LogLevel.info:
106 return False
107 dflag = event.get('dflag')
108 if dflag is False : return False
109 if dflag in debug_set: return False
110 if dflag is None and DBG.TWISTED in debug_set: return False
111 return True
112 except Exception:
02a201e1
IJ
113 print('EXCEPTION (IN BORINGTWISTED CHECK)',
114 traceback.format_exc(), file=org_stderr)
80e963a1
IJ
115 return False
116
b83d422a
IJ
117@implementer(twisted.logger.ILogFilterPredicate)
118class LogNotBoringTwisted:
119 def __call__(self, event):
80e963a1
IJ
120 return (
121 twisted.logger.PredicateResult.no
122 if logevent_is_boringtwisted(event) else
123 twisted.logger.PredicateResult.yes
124 )
b83d422a
IJ
125
126#---------- default config ----------
127
ca732796 128defcfg = '''
71f9ddb6 129[COMMON]
9e445690
IJ
130max_batch_down = 65536
131max_queue_time = 10
132target_requests_outstanding = 3
133http_timeout = 30
134http_timeout_grace = 5
135max_requests_outstanding = 6
136max_batch_up = 4000
137http_retry = 5
c7fb640e 138port = 80
8d374606 139vroutes = ''
d72f8360
IJ
140ifname_client = hippo%%d
141ifname_server = shippo%%d
ca732796
IJ
142
143#[server] or [<client>] overrides
d72f8360 144ipif = userv root ipif %(local)s,%(peer)s,%(mtu)s,slip,%(ifname)s %(rnets)s
ca732796 145
9e445690 146# relating to virtual network
ca732796 147mtu = 1500
ca732796 148
9e445690 149# addrs = 127.0.0.1 ::1
9e445690
IJ
150# url
151
152# relating to virtual network
8d374606
IJ
153vvnetwork = 172.24.230.192
154# vnetwork = <prefix>/<len>
c57e18a7 155# vaddr = <ipaddr>
8d374606 156# vrelay = <ipaddr>
9e445690 157
ca732796
IJ
158
159# [<client-ip4-or-ipv6-address>]
160# password = <password> # used by both, must match
161
c7fb640e 162[LIMIT]
9e445690
IJ
163max_batch_down = 262144
164max_queue_time = 121
165http_timeout = 121
166target_requests_outstanding = 10
ca732796
IJ
167'''
168
87a7c0c7 169# these need to be defined here so that they can be imported by import *
cae50358 170cfg = ConfigParser(strict=False)
ae7c7784
IJ
171optparser = OptionParser()
172
e4006ac4 173_mimetrans = bytes.maketrans(b'-'+slip.esc, slip.esc+b'-')
7b07f0b5
IJ
174def mime_translate(s):
175 # SLIP-encoded packets cannot contain ESC ESC.
176 # Swap `-' and ESC. The result cannot contain `--'
177 return s.translate(_mimetrans)
178
87a7c0c7 179class ConfigResults:
c7fb640e
IJ
180 def __init__(self):
181 pass
87a7c0c7
IJ
182 def __repr__(self):
183 return 'ConfigResults('+repr(self.__dict__)+')'
184
a8827d59 185def log_discard(packet, iface, saddr, daddr, why):
b68c0739 186 log_debug(DBG.DROP,
a8827d59 187 'discarded packet [%s] %s -> %s: %s' % (iface, saddr, daddr, why),
b68c0739 188 d=packet)
1321ad5f 189
b0cfbfce
IJ
190#---------- packet parsing ----------
191
192def packet_addrs(packet):
193 version = packet[0] >> 4
194 if version == 4:
195 addrlen = 4
196 saddroff = 3*4
197 factory = ipaddress.IPv4Address
198 elif version == 6:
199 addrlen = 16
200 saddroff = 2*4
201 factory = ipaddress.IPv6Address
202 else:
203 raise ValueError('unsupported IP version %d' % version)
204 saddr = factory(packet[ saddroff : saddroff + addrlen ])
205 daddr = factory(packet[ saddroff + addrlen : saddroff + addrlen*2 ])
206 return (saddr, daddr)
207
208#---------- address handling ----------
209
210def ipaddr(input):
211 try:
212 r = ipaddress.IPv4Address(input)
213 except AddressValueError:
214 r = ipaddress.IPv6Address(input)
215 return r
216
217def ipnetwork(input):
218 try:
219 r = ipaddress.IPv4Network(input)
220 except NetworkValueError:
221 r = ipaddress.IPv6Network(input)
222 return r
040ff511
IJ
223
224#---------- ipif (SLIP) subprocess ----------
225
a95cfeb2 226class SlipStreamDecoder():
db6ba584 227 def __init__(self, desc, on_packet):
040ff511 228 self._buffer = b''
a95cfeb2 229 self._on_packet = on_packet
db6ba584
IJ
230 self._desc = desc
231 self._log('__init__')
232
233 def _log(self, msg, **kwargs):
3297cac1 234 log_debug(DBG.SLIP_FULL, 'slip %s: %s' % (self._desc, msg), **kwargs)
a95cfeb2
IJ
235
236 def inputdata(self, data):
db6ba584 237 self._log('inputdata', d=data)
7fa9c132
IJ
238 data = self._buffer + data
239 self._buffer = b''
240 packets = slip.decode(data, True)
040ff511
IJ
241 self._buffer = packets.pop()
242 for packet in packets:
a95cfeb2 243 self._maybe_packet(packet)
54890d4d 244 self._log('bufremain', d=self._buffer)
a95cfeb2
IJ
245
246 def _maybe_packet(self, packet):
54890d4d 247 self._log('maybepacket', d=packet)
db6ba584
IJ
248 if len(packet):
249 self._on_packet(packet)
a95cfeb2 250
4f991c0c 251 def flush(self):
54890d4d 252 self._log('flush')
7fa9c132 253 data = self._buffer
a95cfeb2 254 self._buffer = b''
7fa9c132
IJ
255 packets = slip.decode(data)
256 assert(len(packets) == 1)
257 self._maybe_packet(packets[0])
4f991c0c 258
e4006ac4 259class _IpifProcessProtocol(twisted.internet.protocol.ProcessProtocol):
4f991c0c
IJ
260 def __init__(self, router):
261 self._router = router
db6ba584 262 self._decoder = SlipStreamDecoder('ipif', self.slip_on_packet)
a95cfeb2
IJ
263 def connectionMade(self): pass
264 def outReceived(self, data):
265 self._decoder.inputdata(data)
266 def slip_on_packet(self, packet):
4f991c0c
IJ
267 (saddr, daddr) = packet_addrs(packet)
268 if saddr.is_link_local or daddr.is_link_local:
a8827d59 269 log_discard(packet, 'ipif', saddr, daddr, 'link-local')
4f991c0c
IJ
270 return
271 self._router(packet, saddr, daddr)
040ff511
IJ
272 def processEnded(self, status):
273 status.raiseException()
274
275def start_ipif(command, router):
040ff511
IJ
276 ipif = _IpifProcessProtocol(router)
277 reactor.spawnProcess(ipif,
278 '/bin/sh',['sh','-xc', command],
ff613365
IJ
279 childFDs={0:'w', 1:'r', 2:2},
280 env=None)
909e0ff3 281 return ipif
040ff511 282
909e0ff3 283def queue_inbound(ipif, packet):
15407d80 284 log_debug(DBG.FLOW, "queue_inbound", d=packet)
040ff511
IJ
285 ipif.transport.write(slip.delimiter)
286 ipif.transport.write(slip.encode(packet))
287 ipif.transport.write(slip.delimiter)
288
650a3251
IJ
289#---------- packet queue ----------
290
291class PacketQueue():
d579a048
IJ
292 def __init__(self, desc, max_queue_time):
293 self._desc = desc
8718b02c 294 assert(desc + '')
650a3251
IJ
295 self._max_queue_time = max_queue_time
296 self._pq = collections.deque() # packets
297
b68c0739 298 def _log(self, dflag, msg, **kwargs):
8c3b6620 299 log_debug(dflag, self._desc+' pq: '+msg, **kwargs)
d579a048 300
650a3251 301 def append(self, packet):
8c3b6620 302 self._log(DBG.QUEUE, 'append', d=packet)
650a3251
IJ
303 self._pq.append((time.monotonic(), packet))
304
305 def nonempty(self):
8c3b6620 306 self._log(DBG.QUEUE, 'nonempty ?')
650a3251
IJ
307 while True:
308 try: (queuetime, packet) = self._pq[0]
8c3b6620
IJ
309 except IndexError:
310 self._log(DBG.QUEUE, 'nonempty ? empty.')
311 return False
650a3251
IJ
312
313 age = time.monotonic() - queuetime
84e763c7 314 if age > self._max_queue_time:
650a3251 315 # strip old packets off the front
8c3b6620 316 self._log(DBG.QUEUE, 'dropping (old)', d=packet)
650a3251
IJ
317 self._pq.popleft()
318 continue
319
8c3b6620 320 self._log(DBG.QUEUE, 'nonempty ? nonempty.')
650a3251
IJ
321 return True
322
7b07f0b5
IJ
323 def process(self, sizequery, moredata, max_batch):
324 # sizequery() should return size of batch so far
325 # moredata(s) should add s to batch
8c3b6620 326 self._log(DBG.QUEUE, 'process...')
7b07f0b5
IJ
327 while True:
328 try: (dummy, packet) = self._pq[0]
8c3b6620
IJ
329 except IndexError:
330 self._log(DBG.QUEUE, 'process... empty')
331 break
332
333 self._log(DBG.QUEUE_CTRL, 'process... packet', d=packet)
7b07f0b5
IJ
334
335 encoded = slip.encode(packet)
336 sofar = sizequery()
337
8c3b6620
IJ
338 self._log(DBG.QUEUE_CTRL,
339 'process... (sofar=%d, max=%d) encoded' % (sofar, max_batch),
b68c0739 340 d=encoded)
8c3b6620 341
7b07f0b5
IJ
342 if sofar > 0:
343 if sofar + len(slip.delimiter) + len(encoded) > max_batch:
8c3b6620 344 self._log(DBG.QUEUE_CTRL, 'process... overflow')
7b07f0b5
IJ
345 break
346 moredata(slip.delimiter)
347
348 moredata(encoded)
84e763c7 349 self._pq.popleft()
ae7c7784
IJ
350
351#---------- error handling ----------
352
b68c0739
IJ
353_crashing = False
354
ae7c7784 355def crash(err):
b68c0739
IJ
356 global _crashing
357 _crashing = True
e8ed0029
IJ
358 print('========== CRASH ==========', err,
359 '===========================', file=sys.stderr)
ae7c7784
IJ
360 try: reactor.stop()
361 except twisted.internet.error.ReactorNotRunning: pass
362
363def crash_on_defer(defer):
364 defer.addErrback(lambda err: crash(err))
365
e4006ac4 366def crash_on_critical(event):
ae7c7784
IJ
367 if event.get('log_level') >= LogLevel.critical:
368 crash(twisted.logger.formatEvent(event))
369
87a7c0c7
IJ
370#---------- config processing ----------
371
c7fb640e
IJ
372def _cfg_process_putatives():
373 servers = { }
374 clients = { }
375 # maps from abstract object to canonical name for cs's
87a7c0c7 376
c7fb640e
IJ
377 def putative(cmap, abstract, canoncs):
378 try:
379 current_canoncs = cmap[abstract]
380 except KeyError:
381 pass
382 else:
383 assert(current_canoncs == canoncs)
384 cmap[abstract] = canoncs
385
386 server_pat = r'[-.0-9A-Za-z]+'
387 client_pat = r'[.:0-9a-f]+'
388 server_re = regexp.compile(server_pat)
6d5e8381
IJ
389 serverclient_re = regexp.compile(
390 server_pat + r' ' + '(?:' + client_pat + '|LIMIT)')
88487243 391
c7fb640e 392 for cs in cfg.sections():
3a8ed92d
IJ
393 def dbg(m):
394 log_debug_config('putatives: section [%s] %s' % (cs, m))
4652e382 395
43dd2ce0 396 def log_ignore(why):
3a8ed92d 397 dbg('X ignore: %s' % (why))
43dd2ce0
IJ
398 print('warning: ignoring config section [%s] (%s)' % (cs, why),
399 file=sys.stderr)
400
71f9ddb6
IJ
401 if cs == 'LIMIT' or cs == 'COMMON':
402 # plan A "[LIMIT]" or "[COMMON]"
3a8ed92d 403 dbg('A ignore')
c7fb640e 404 continue
88487243 405
c7fb640e
IJ
406 try:
407 # plan B "[<client>]" part 1
408 ci = ipaddr(cs)
409 except AddressValueError:
88487243 410
c7fb640e
IJ
411 if server_re.fullmatch(cs):
412 # plan C "[<servername>]"
3a8ed92d 413 dbg('C <server>')
c7fb640e
IJ
414 putative(servers, cs, cs)
415 continue
416
417 if serverclient_re.fullmatch(cs):
418 # plan D "[<servername> <client>]" part 1
419 (pss,pcs) = cs.split(' ')
420
8d374606 421 if pcs == 'LIMIT':
c7fb640e 422 # plan E "[<servername> LIMIT]"
3a8ed92d 423 dbg('E <server> LIMIT')
c7fb640e
IJ
424 continue
425
426 try:
427 # plan D "[<servername> <client>]" part 2
b8c38e66 428 ci = ipaddr(pcs)
c7fb640e 429 except AddressValueError:
343c8cf4 430 # plan F branch 1 "[<some thing we do not understand>]"
43dd2ce0 431 log_ignore('bad-addr')
c7fb640e
IJ
432 continue
433
434 else: # no AddressValueError
4652e382 435 # plan D "[<servername> <client>]" part 3
3a8ed92d 436 dbg('D <server> <client>')
c7fb640e
IJ
437 putative(clients, ci, pcs)
438 putative(servers, pss, pss)
439 continue
343c8cf4
IJ
440 else:
441 # plan F branch 2 "[<some thing we do not understand>]"
442 log_ignore('nomatch '+ repr(serverclient_re))
c7fb640e
IJ
443
444 else: # no AddressValueError
445 # plan B "[<client>" part 2
3a8ed92d 446 dbg('B <client>')
c7fb640e
IJ
447 putative(clients, ci, cs)
448 continue
449
450 return (servers, clients)
451
62d13acc 452def cfg_process_general(c, ss):
300fe4ed 453 c.mtu = cfg1getint(ss, 'mtu')
c7fb640e
IJ
454
455def cfg_process_saddrs(c, ss):
456 class ServerAddr():
457 def __init__(self, port, addrspec):
458 self.port = port
459 # also self.addr
460 try:
461 self.addr = ipaddress.IPv4Address(addrspec)
462 self._endpointfactory = twisted.internet.endpoints.TCP4ServerEndpoint
463 self._inurl = b'%s'
464 except AddressValueError:
465 self.addr = ipaddress.IPv6Address(addrspec)
466 self._endpointfactory = twisted.internet.endpoints.TCP6ServerEndpoint
467 self._inurl = b'[%s]'
468 def make_endpoint(self):
3b69fba9
IJ
469 return self._endpointfactory(reactor, self.port,
470 interface= '%s' % self.addr)
c7fb640e
IJ
471 def url(self):
472 url = b'http://' + (self._inurl % str(self.addr).encode('ascii'))
473 if self.port != 80: url += b':%d' % self.port
474 url += b'/'
475 return url
3b69fba9
IJ
476 def __repr__(self):
477 return 'ServerAddr'+repr((self.port,self.addr))
c7fb640e 478
300fe4ed 479 c.port = cfg1getint(ss,'port')
c7fb640e 480 c.saddrs = [ ]
300fe4ed 481 for addrspec in cfg1get(ss, 'addrs').split():
c7fb640e
IJ
482 sa = ServerAddr(c.port, addrspec)
483 c.saddrs.append(sa)
484
485def cfg_process_vnetwork(c, ss):
300fe4ed 486 c.vnetwork = ipnetwork(cfg1get(ss,'vnetwork'))
c7f134ce
IJ
487 if c.vnetwork.num_addresses < 3 + 2:
488 raise ValueError('vnetwork needs at least 2^3 addresses')
88487243 489
8d374606 490def cfg_process_vaddr(c, ss):
88487243 491 try:
300fe4ed 492 c.vaddr = cfg1get(ss,'vaddr')
88487243 493 except NoOptionError:
8d374606 494 cfg_process_vnetwork(c, ss)
c7f134ce 495 c.vaddr = next(c.vnetwork.hosts())
88487243 496
c7fb640e
IJ
497def cfg_search_section(key,sections):
498 for section in sections:
499 if cfg.has_option(section, key):
500 return section
8d374606 501 raise NoOptionError(key, repr(sections))
c7fb640e 502
fa63bd93
IJ
503def cfg_get_raw(*args, **kwargs):
504 # for passing to cfg_search
505 return cfg.get(*args, raw=True, **kwargs)
506
c7fb640e
IJ
507def cfg_search(getter,key,sections):
508 section = cfg_search_section(key,sections)
509 return getter(section, key)
510
71f9ddb6
IJ
511def cfg1get(section,key, getter=cfg.get,**kwargs):
512 section = cfg_search_section(key,[section,'COMMON'])
513 return getter(section,key,**kwargs)
300fe4ed 514
71f9ddb6
IJ
515def cfg1getint(section,key, **kwargs):
516 return cfg1get(section,key, getter=cfg.getint,**kwargs);
300fe4ed 517
c7fb640e 518def cfg_process_client_limited(cc,ss,sections,key):
300fe4ed
IJ
519 val = cfg_search(cfg1getint, key, sections)
520 lim = cfg_search(cfg1getint, key, ['%s LIMIT' % ss, 'LIMIT'])
c7fb640e
IJ
521 cc.__dict__[key] = min(val,lim)
522
523def cfg_process_client_common(cc,ss,cs,ci):
524 # returns sections to search in, iff password is defined, otherwise None
525 cc.ci = ci
526
8d374606 527 sections = ['%s %s' % (ss,cs),
c7fb640e
IJ
528 cs,
529 ss,
71f9ddb6 530 'COMMON']
c7fb640e
IJ
531
532 try: pwsection = cfg_search_section('password', sections)
533 except NoOptionError: return None
88487243 534
300fe4ed 535 pw = cfg1get(pwsection, 'password')
c7f134ce 536 cc.password = pw.encode('utf-8')
88487243 537
c7fb640e
IJ
538 cfg_process_client_limited(cc,ss,sections,'target_requests_outstanding')
539 cfg_process_client_limited(cc,ss,sections,'http_timeout')
88487243 540
c7fb640e
IJ
541 return sections
542
8d374606 543def cfg_process_ipif(c, sections, varmap):
c7fb640e
IJ
544 for d, s in varmap:
545 try: v = getattr(c, s)
546 except AttributeError: continue
547 setattr(c, d, v)
548
c7f134ce 549 #print('CFGIPIF',repr((varmap, sections, c.__dict__)),file=sys.stderr)
8d374606 550
c7fb640e 551 section = cfg_search_section('ipif', sections)
300fe4ed 552 c.ipif_command = cfg1get(section,'ipif', vars=c.__dict__)
88487243 553
ae7c7784
IJ
554#---------- startup ----------
555
8c771381
IJ
556def log_debug_config(m):
557 if not DBG.CONFIG in debug_set: return
558 print('DBG.CONFIG:', m)
559
5510890e 560def common_startup(process_cfg):
c7fb640e
IJ
561 # calls process_cfg(putative_clients, putative_servers)
562
82302bac 563 # ConfigParser hates #-comments after values
c7fb640e 564 trailingcomments_re = regexp.compile(r'#.*')
82302bac 565 cfg.read_string(trailingcomments_re.sub('', defcfg))
cae50358
IJ
566 need_defcfg = True
567
568 def readconfig(pathname, mandatory=True):
569 def log(m, p=pathname):
570 if not DBG.CONFIG in debug_set: return
00ea5443 571 log_debug_config('%s: %s' % (m, p))
cae50358
IJ
572
573 try:
574 files = os.listdir(pathname)
575
576 except FileNotFoundError:
577 if mandatory: raise
578 log('skipped')
579 return
580
581 except NotADirectoryError:
582 cfg.read(pathname)
583 log('read file')
584 return
585
586 # is a directory
587 log('directory')
588 re = regexp.compile('[^-A-Za-z0-9_]')
2b13e1cc 589 for f in os.listdir(pathname):
cae50358
IJ
590 if re.search(f): continue
591 subpath = pathname + '/' + f
592 try:
593 os.stat(subpath)
594 except FileNotFoundError:
595 log('entry skipped', subpath)
596 continue
597 cfg.read(subpath)
598 log('entry read', subpath)
599
600 def oc_config(od,os, value, op):
601 nonlocal need_defcfg
602 need_defcfg = False
603 readconfig(value)
2e68eb10 604
26f04eff
IJ
605 def oc_extra_config(od,os, value, op):
606 readconfig(value)
607
7852bfc8
IJ
608 def read_defconfig():
609 readconfig('/etc/hippotat/config.d', False)
610 readconfig('/etc/hippotat/passwords.d', False)
611 readconfig('/etc/hippotat/master.cfg', False)
612
26f04eff
IJ
613 def oc_defconfig(od,os, value, op):
614 nonlocal need_defcfg
615 need_defcfg = False
616 read_defconfig(value)
617
9acb0eca
IJ
618 def dfs_less_detailed(dl):
619 return [df for df in DBG.iterconstants() if df <= dl]
620
621 def ds_default(od,os,dl,op):
2e68eb10 622 global debug_set
ff0fc3fa
IJ
623 debug_set.clear
624 debug_set |= set(dfs_less_detailed(debug_def_detail))
2e68eb10 625
9acb0eca 626 def ds_select(od,os, spec, op):
9acb0eca
IJ
627 for it in spec.split(','):
628
9acb0eca
IJ
629 if it.startswith('-'):
630 mutator = debug_set.discard
631 it = it[1:]
632 else:
633 mutator = debug_set.add
2cf75145
IJ
634
635 if it == '+':
636 dfs = DBG.iterconstants()
637
638 else:
639 if it.endswith('+'):
640 mapper = dfs_less_detailed
641 it = it[0:len(it)-1]
642 else:
643 mapper = lambda x: [x]
644
645 try:
646 dfspec = DBG.lookupByName(it)
647 except ValueError:
648 optparser.error('unknown debug flag %s in --debug-select' % it)
649
650 dfs = mapper(dfspec)
651
652 for df in dfs:
653 mutator(df)
9acb0eca
IJ
654
655 optparser.add_option('-D', '--debug',
2e68eb10
IJ
656 nargs=0,
657 action='callback',
9acb0eca
IJ
658 help='enable default debug (to stdout)',
659 callback= ds_default)
660
661 optparser.add_option('--debug-select',
662 nargs=1,
663 type='string',
2cf75145 664 metavar='[-]DFLAG[+]|[-]+,...',
9acb0eca 665 help=
2cf75145
IJ
666'''enable (`-': disable) each specified DFLAG;
667`+': do same for all "more interesting" DFLAGSs;
668just `+': all DFLAGs.
669 DFLAGS: ''' + ' '.join([df.name for df in DBG.iterconstants()]),
9acb0eca
IJ
670 action='callback',
671 callback= ds_select)
2e68eb10 672
cae50358
IJ
673 optparser.add_option('-c', '--config',
674 nargs=1,
675 type='string',
676 metavar='CONFIGFILE',
677 dest='configfile',
678 action='callback',
679 callback= oc_config)
680
26f04eff
IJ
681 optparser.add_option('--extra-config',
682 nargs=1,
683 type='string',
684 metavar='CONFIGFILE',
685 dest='configfile',
686 action='callback',
687 callback= oc_extra_config)
688
689 optparser.add_option('--default-config',
690 action='callback',
691 callback= oc_defconfig)
692
f022d67f
IJ
693 (opts, args) = optparser.parse_args()
694 if len(args): optparser.error('no non-option arguments please')
2e68eb10 695
cae50358 696 if need_defcfg:
7852bfc8 697 read_defconfig()
9acb0eca 698
c7fb640e 699 try:
8d374606 700 (pss, pcs) = _cfg_process_putatives()
1cc6968f 701 process_cfg(opts, pss, pcs)
5510890e
IJ
702 except (configparser.Error, ValueError):
703 traceback.print_exc(file=sys.stderr)
704 print('\nInvalid configuration, giving up.', file=sys.stderr)
705 sys.exit(12)
706
ff0fc3fa
IJ
707
708 #print('X', debug_set, file=sys.stderr)
2e68eb10 709
8c3b6620 710 log_formatter = twisted.logger.formatEventAsClassicLogText
389236df
IJ
711 stdout_obs = twisted.logger.FileLogObserver(sys.stdout, log_formatter)
712 stderr_obs = twisted.logger.FileLogObserver(sys.stderr, log_formatter)
713 pred = twisted.logger.LogLevelFilterPredicate(LogLevel.error)
b83d422a 714 stdsomething_obs = twisted.logger.FilteringLogObserver(
389236df
IJ
715 stderr_obs, [pred], stdout_obs
716 )
ec2c9312
IJ
717 global file_log_observer
718 file_log_observer = twisted.logger.FilteringLogObserver(
b83d422a
IJ
719 stdsomething_obs, [LogNotBoringTwisted()]
720 )
721 #log_observer = stdsomething_obs
8c3b6620 722 twisted.logger.globalLogBeginner.beginLoggingTo(
ec2c9312 723 [ file_log_observer, crash_on_critical ]
8c3b6620 724 )
ae7c7784 725
ae7c7784 726def common_run():
b68c0739
IJ
727 log_debug(DBG.INIT, 'entering reactor')
728 if not _crashing: reactor.run()
2eecd19c 729 print('ENDED', file=sys.stderr)
207f5042 730 sys.exit(16)