The Assimilation Project  based on Assimilation version 1.1.7.1474836767
dispatchtarget.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # vim: smartindent number tabstop=4 shiftwidth=4 expandtab colorcolumn=100
3 #
4 # This file is part of the Assimilation Project.
5 #
6 # Copyright (C) 2011, 2012 - Alan Robertson <alanr@unix.sh>
7 #
8 # The Assimilation software is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # The Assimilation software is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with the Assimilation Project software. If not, see http://www.gnu.org/licenses/
20 #
21 '''
22 This file is responsible for a variety of dispatch classes - for handling all our
23 various types of incoming packets.
24 '''
25 
26 import sys
27 sys.path.append("cma")
28 from cmaconfig import ConfigFile
29 from cmadb import CMAdb
30 from frameinfo import FrameSetTypes, FrameTypes
31 from AssimCclasses import pyNetAddr, pyConfigContext, pySwitchDiscovery, pyCryptFrame
32 from AssimCtypes import cryptcurve25519_save_public_key, DEFAULT_FSP_QID
33 from monitoring import MonitorAction
34 from assimevent import AssimEvent
35 
36 class DispatchTarget(object):
37  '''Base class for handling incoming FrameSets.
38  This base class is designated to handle unhandled FrameSets.
39  All it does is print that we received them.
40  '''
41  dispatchtable = {}
42  def __init__(self):
43  'Constructor for base class DispatchTarget'
44  from droneinfo import Drone
45  self.droneinfo = Drone # Get around Import loops...
46  self.io = None
47  self.config = None
48 
49  def dispatch(self, origaddr, frameset):
50  'Dummy dispatcher for base class DispatchTarget - for unhandled pyFrameSets'
51  self = self # Make pylint happy...
52  fstype = frameset.get_framesettype()
53  CMAdb.log.info("Received unhandled FrameSet of type [%s] from [%s]"
54  % (FrameSetTypes.get(fstype)[0], str(origaddr)))
55  print ("Received unhandled FrameSet of type [%d:%s] from [%s]"
56  % (fstype, FrameSetTypes.get(fstype)[0], str(origaddr)))
57  for frame in frameset.iter():
58  frametype = frame.frametype()
59  print "\tframe type [%s]: [%s]" \
60  % (FrameTypes.get(frametype)[1], str(frame))
61 
62  def setconfig(self, io, config):
63  'Save away our IO object and our configuration'
64  self.io = io
65  self.config = config
66 
67  @staticmethod
68  def register(classtoregister):
69  '''Register the given class in DispatchTarget.dispatchtable
70  This function is intended to be used as a decorator.
71  This is requires that the class being registered be named
72  Dispatch{name-of-message-being-dispatched}
73  '''
74  cname = classtoregister.__name__
75  if not cname.startswith('Dispatch'):
76  raise(ValueError('Dispatch class names must start with "Dispatch"'))
77  msgname = cname[8:]
78  # This is kinda cool!
79  DispatchTarget.dispatchtable[FrameSetTypes.get(msgname)[0]] = classtoregister()
80  return classtoregister
81 
82 
83 
84 @DispatchTarget.register
86  'DispatchTarget subclass for handling incoming HBDEAD FrameSets.'
87 
88  def dispatch(self, origaddr, frameset):
89  'Dispatch function for HBDEAD FrameSets'
90  #fromdrone = self.droneinfo.find(origaddr)
91  #fstype = frameset.get_framesettype()
92  #CMAdb.log.warning("DispatchHBDEAD: received [%s] FrameSet from [%s] [%s]"
93  #% (FrameSetTypes.get(fstype)[0], str(origaddr), fromdrone.designation))
94  for frame in frameset.iter():
95  frametype = frame.frametype()
96  if frametype == FrameTypes.IPPORT:
97  deaddrone = self.droneinfo.find(frame.getnetaddr())
98  if deaddrone.status == 'up':
99  CMAdb.log.warning("DispatchHBDEAD: Drone@%s is dead(%s)"
100  % (frame.getnetaddr(), deaddrone))
101  if CMAdb.debug:
102  CMAdb.log.debug("DispatchHBDEAD: [%s] is the guy who died!" % deaddrone)
103  deaddrone.death_report('dead', 'HBDEAD packet received', origaddr, frameset)
104 
105 @DispatchTarget.register
107  'DispatchTarget subclass for handling incoming HBSHUTDOWN FrameSets.'
108  def dispatch(self, origaddr, frameset):
109  'Dispatch function for HBSHUTDOWN FrameSets'
110  fstype = frameset.get_framesettype()
111  fsname = FrameSetTypes.get(fstype)[0]
112  for frame in frameset.iter():
113  frametype = frame.frametype()
114  if frametype == FrameTypes.HOSTNAME:
115  hostname = frame.getstr()
116  fromdrone = self.droneinfo.find(hostname, port=origaddr.port())
117  if fromdrone is not None:
118  CMAdb.log.info("System %s at %s reports graceful shutdown."
119  % (hostname, str(origaddr)))
120  print >> sys.stderr, ("System %s at %s reports graceful shutdown."
121  % (hostname, str(origaddr)))
122  fromdrone.death_report('dead', fsname, origaddr, frameset)
123  else:
124  CMAdb.log.error(
125  "DispatchHBSHUTDOWN: received %s FrameSet from unknown drone %s at [%s]"
126  % (fsname, hostname, str(origaddr)))
127  return
128  CMAdb.log.error("DispatchHBSHUTDOWN: received invalid %s FrameSet from drone at [%s]"
129  % (fsname, str(origaddr)))
130  CMAdb.log.error("DispatchHBSHUTDOWN: invalid FrameSet: %s", str(frameset))
131 
132 
133 # pylint: disable=R0914,R0912
134 @DispatchTarget.register
136  'DispatchTarget subclass for handling incoming STARTUP FrameSets.'
137  def dispatch(self, origaddr, frameset):
138  json = None
139  addrstr = repr(origaddr)
140  fstype = frameset.get_framesettype()
141  localtime = None
142  listenaddr = None
143  keyid = None
144  pubkey = None
145  keysize = None
146 
147  #print >> sys.stderr, ("DispatchSTARTUP: received [%s] FrameSet from [%s]"
148  #% (FrameSetTypes.get(fstype)[0], addrstr))
149  if CMAdb.debug:
150  CMAdb.log.debug("DispatchSTARTUP: received [%s] FrameSet from [%s]"
151  % (FrameSetTypes.get(fstype)[0], addrstr))
152  if not self.io.connactive(origaddr):
153  self.io.closeconn(DEFAULT_FSP_QID, origaddr)
154  CMAdb.transaction.post_transaction_packets.append(FrameSetTypes.ACKSTARTUP)
155  for frame in frameset.iter():
156  frametype = frame.frametype()
157  if frametype == FrameTypes.WALLCLOCK:
158  localtime = str(frame.getint())
159  elif frametype == FrameTypes.IPPORT:
160  listenaddr = frame.getnetaddr()
161  elif frametype == FrameTypes.HOSTNAME:
162  sysname = frame.getstr()
163  if sysname == CMAdb.nodename:
164  if origaddr.islocal():
165  CMAdb.log.info("Received STARTUP from local system (%s)" % addrstr)
166  else:
167  addresses = ['127.0.0.1', '::ffff:127.0.0.1', '::1' ]
168  for address in addresses:
169  localhost = pyNetAddr(address)
170  self.io.addalias(localhost, origaddr)
171  CMAdb.log.info("Aliasing %s to %s" % (localhost, origaddr))
172  elif frametype == FrameTypes.JSDISCOVER:
173  json = frame.getstr()
174  #print >> sys.stderr, 'GOT JSDISCOVER JSON: [%s] (strlen:%s,framelen:%s)' \
175  #% (json, len(json), frame.framelen())
176  elif frametype == FrameTypes.KEYID:
177  keyid = frame.getstr()
178  elif frametype == FrameTypes.PUBKEYCURVE25519:
179  pubkey = frame.framevalue()
180  keysize = frame.framelen()
181 
182  joininfo = pyConfigContext(init=json)
183  origaddr, isNAT = self.validate_source_ip(sysname, origaddr, joininfo, listenaddr)
184 
185 
186  CMAdb.log.info('Drone %s registered from address %s (%s) port %s, key_id %s'
187  % (sysname, origaddr, addrstr, origaddr.port(), keyid))
188  drone = self.droneinfo.add(sysname, 'STARTUP packet', port=origaddr.port()
189  , primary_ip_addr=str(origaddr))
190  drone.listenaddr = str(listenaddr) # Seems good to hang onto this...
191  drone.isNAT = isNAT # ditto...
192  if CMAdb.debug:
193  CMAdb.log.debug('DRONE select_ip() result: %s' % (drone.select_ip()))
194  CMAdb.log.debug('DRONE listenaddr: %s' % (drone.listenaddr))
195  CMAdb.log.debug('DRONE port: %s (%s)' % (drone.port, type(drone.port)))
196  # Did they give us the crypto info we need?
197  if keyid is None or pubkey is None:
198  if CMAdb.debug:
199  CMAdb.log.debug('Drone %s registered with keyid %s and pubkey provided: %s'
200  % (self, keyid, pubkey is not None))
201  else:
202  if drone.key_id == '':
203  if not keyid.startswith(sysname + "@@"):
204  CMAdb.log.warning("Drone %s wants to register with key_id %s -- permitted."
205  , sysname, keyid)
206  if not cryptcurve25519_save_public_key(keyid, pubkey, keysize):
207  raise ValueError("Drone %s public key (key_id %s, %d bytes) is invalid."
208  % (sysname, keyid, keysize))
209  elif drone.key_id != keyid:
210  raise ValueError("Drone %s tried to register with key_id %s instead of %s."
211  % (sysname, keyid, drone.key_id))
212  drone.set_crypto_identity(keyid=keyid)
213  pyCryptFrame.dest_set_key_id(origaddr, keyid)
214  #
215  # THIS IS HERE BECAUSE OF A PROTOCOL BUG...
216  # @FIXME Protocol bug when starting up a connection if our first (this) packet gets lost,
217  # then the protocol doesn't retransmit it.
218  # More specifically, it seems to clear it out of the queue.
219  # This might be CMA bug or a protocol bug. It's not clear...
220  # The packet goes into the queue, but if that packet is lost in transmission, then when
221  # we come back around here, it's not in the queue any more, even though it
222  # definitely wasn't ACKed.
223  # Once this is fixed, this "add_packet" call needs to go *after* the 'if' statement below.
224  #
225  CMAdb.transaction.add_packet(origaddr, FrameSetTypes.SETCONFIG, (str(self.config), )
226  , FrameTypes.CONFIGJSON)
227 
228  if (localtime is not None):
229  if (drone.lastjoin == localtime):
230  CMAdb.log.warning('Drone %s [%s] sent duplicate STARTUP' % (sysname, origaddr))
231  if CMAdb.debug:
232  self.io.log_conn(origaddr)
233  return
234  drone.lastjoin = localtime
235  #print >> sys.stderr, 'DRONE from find: ', drone, type(drone), drone.port
236 
237  drone.startaddr = str(origaddr)
238  if json is not None:
239  drone.logjson(origaddr, json)
240  if CMAdb.debug:
241  CMAdb.log.debug('Joining TheOneRing: %s / %s / %s' % (drone, type(drone), drone.port))
242  CMAdb.cdb.TheOneRing.join(drone)
243  if CMAdb.debug:
244  CMAdb.log.debug('Requesting Discovery from %s' % str(drone))
245  discovery_params = []
246  for agent in self.config['initial_discovery']:
247  params = ConfigFile.agent_params(self.config, 'discovery', agent, sysname)
248  params['agent'] = agent
249  params['instance'] = '_init_%s' % agent
250  discovery_params.append(params)
251  # Discover the permissions of all the lists of files we're configured to ask about
252  # Note that there are several lists to keep the amount of data in any one list
253  # down to a somewhat more reasonable level. 'fileattrs' output is really verbose
254  for pathlist_name in self.config['perm_discovery_lists']:
255  paths = self.config[pathlist_name]
256  params = ConfigFile.agent_params(self.config, 'discovery', 'fileattrs', sysname)
257  params['agent'] = 'fileattrs'
258  params['instance'] = pathlist_name
259  params['parameters'] = {'ASSIM_filelist': paths}
260  discovery_params.append(params)
261  if CMAdb.debug:
262  CMAdb.log.debug('Discovery details: %s' % str(discovery_params))
263  for item in discovery_params:
264  CMAdb.log.debug('Discovery item details: %s' % str(item))
265  drone.request_discovery(discovery_params)
266  AssimEvent(drone, AssimEvent.OBJUP)
267 
268  @staticmethod
269  def validate_source_ip(sysname, origaddr, jsobj, listenaddr):
270  '''
271  This chunk of code is kinda stupid...
272  There is a docker/NAT bug where it screws up the source address of multicast packets
273  This code detects that that has happened and works around it...
274  '''
275  # Local addresses aren't NATted, but the code below will think so...
276  if origaddr.islocal():
277  return origaddr, False
278  match = False
279  isNAT = False
280  jsdata = jsobj['data']
281  canonorig = str(pyNetAddr(origaddr).toIPv6())
282  primaryip = None
283  for ifname in jsdata:
284  for ip_netmask in jsdata[ifname]['ipaddrs']:
285  ip = ip_netmask.split('/')[0]
286  canonip = pyNetAddr(ip, origaddr.port()).toIPv6()
287  if str(canonip) == canonorig:
288  match = True
289  break
290  ipinfo = jsdata[ifname]['ipaddrs'][ip_netmask]
291  if 'default_gw' in jsdata[ifname] and ipinfo.get('name') == ifname:
292  primaryip = canonip
293  # FIXME: This currently is set up to work around gratuitous NATting in Docker (bug!)
294  # It should evolve to do the right things for real NAT configurations...
295  if not match:
296  CMAdb.log.warning('Drone %s sent STARTUP packet with NATted source address (%s)'
297  % (sysname, origaddr))
298  isNAT = True
299  if primaryip is not None:
300  if CMAdb.running_under_docker():
301  CMAdb.log.warning('Drone %s STARTUP orig address assumed to be (%s)'
302  % (sysname, primaryip))
303  CMAdb.log.warning('Presumed to be due to a known Docker bug.')
304  origaddr = primaryip
305  if listenaddr is not None and primaryip.port() != listenaddr.port():
306  CMAdb.log.warning('Drone %s STARTUP port is NATted: Assumed to be (%s)'
307  % (sysname, listenaddr.port()))
308  origaddr = pyNetAddr(origaddr, port=listenaddr.port())
309  return origaddr, isNAT
310 
311 @DispatchTarget.register
313  '''DispatchTarget subclass for handling incoming HBMARTIAN FrameSets.
314  HBMARTIAN packets occur when a system is receiving unexpected heartbeats from another system.
315 
316  There are a few known causes for them:
317  - The reporting system was slow to act on a request to expect these heartbeats
318  - The MARTIAN source had been erroneously declared dead (network split) with 2 subcases:
319  - It is currently marked as dead - we should resurrect it and add to the ring
320  mark it as alive, and tell it to stop sending
321  UNLESS it's from an HBSHUTDOWN - then it's likely bad timing...
322  - It is currently marked as alive - two subcases:
323  - the reporting system is one of its partners peers - just ignore this
324  - the reporting system is not one of its partners - tell source to stop
325  this can be caused by the system being slower to update
326  '''
327  def dispatch(self, origaddr, frameset):
328  fstype = frameset.get_framesettype()
329  if CMAdb.debug:
330  CMAdb.log.debug("DispatchHBMARTIAN: received [%s] FrameSet from address %s "
331  % (FrameSetTypes.get(fstype)[0], origaddr))
332  reporter = self.droneinfo.find(origaddr) # System receiving the MARTIAN FrameSet
333  martiansrcaddr = None
334  for frame in frameset.iter():
335  frametype = frame.frametype()
336  if frametype == FrameTypes.IPPORT:
337  martiansrcaddr = frame.getnetaddr()
338  break
339  martiansrc = self.droneinfo.find(martiansrcaddr) # Source of MARTIAN event
340  if CMAdb.debug:
341  CMAdb.log.debug("DispatchHBMARTIAN: received [%s] FrameSet from %s/%s about %s/%s"
342  % (FrameSetTypes.get(fstype)[0], reporter, origaddr, martiansrc, martiansrcaddr))
343  if martiansrc.status != 'up':
344  if martiansrc.reason == 'HBSHUTDOWN':
345  # Just bad timing. All is well...
346  return
347  CMAdb.log.info('DispatchHBMARTIAN: %s had been erroneously marked %s; reason %s'
348  % (martiansrc, martiansrc.status, martiansrc.reason))
349  if CMAdb.debug:
350  CMAdb.log.info('DispatchHBMARTIAN: telling %s/%s to stop sending to %s/%s (%s case)'
351  % (martiansrc, martiansrcaddr, reporter, origaddr, martiansrc.status))
352  martiansrc.status='up'
353  martiansrc.reason='HBMARTIAN'
354  martiansrc.send_hbmsg(martiansrcaddr, FrameSetTypes.STOPSENDEXPECTHB, (origaddr,))
355  CMAdb.cdb.TheOneRing.join(martiansrc)
356  AssimEvent(martiansrc, AssimEvent.OBJUP)
357  return
358  # OK, it's alive...
359  if CMAdb.cdb.TheOneRing.are_partners(reporter, martiansrc):
360  if CMAdb.debug:
361  CMAdb.log.debug('DispatchHBMARTIAN: Ignoring msg from %s about %s'
362  % (reporter, martiansrc))
363  else:
364  if CMAdb.debug:
365  CMAdb.log.info('DispatchHBMARTIAN: telling %s/%s to stop sending to %s/%s (%s case)'
366  % (martiansrc, martiansrcaddr, reporter, origaddr, martiansrc.status))
367  # This probably isn't necessary in most cases, but it doesn't hurt anything.
368  # If the offender is just slow to update, he'll catch up...
369  martiansrc.send_hbmsg(martiansrcaddr, FrameSetTypes.STOPSENDEXPECTHB, (origaddr,))
370 
372  '''DispatchTarget subclass for handling incoming HBHBBACKALIVE FrameSets.
373  HBBACKALIVE packets occur when a system hears a heartbeat from a system it thought was dead.
374  '''
375  def dispatch(self, origaddr, frameset):
376  fstype = frameset.get_framesettype()
377  if CMAdb.debug:
378  CMAdb.log.debug("DispatchHBBACKALIVE: received [%s] FrameSet from address %s"
379  % (FrameSetTypes.get(fstype)[0], origaddr))
380  reporter = self.droneinfo.find(origaddr) # System receiving the MARTIAN FrameSet
381  alivesrcaddr = None
382  for frame in frameset.iter():
383  frametype = frame.frametype()
384  if frametype == FrameTypes.IPPORT:
385  alivesrcaddr = frame.getnetaddr()
386  break
387  alivesrc = self.droneinfo.find(alivesrcaddr) # Source of HBBACKALIVE event
388  if CMAdb.debug:
389  CMAdb.log.debug("DispatchHBBACKALIVE: received [%s] FrameSet from %s/%s about %s/%s"
390  % (FrameSetTypes.get(fstype)[0], reporter, origaddr, alivesrc, alivesrcaddr))
391  if alivesrc.status != 'up':
392  if alivesrc.reason == 'HBSHUTDOWN':
393  # Just bad timing. All is well...
394  return
395  CMAdb.log.info('DispatchHBBACKALIVE: %s had been erroneously marked %s; reason %s'
396  % (alivesrc, alivesrc.status, alivesrc.reason))
397  alivesrc.status='up'
398  alivesrc.reason='HBBACKALIVE'
399  CMAdb.cdb.TheOneRing.join(alivesrc)
400  AssimEvent(alivesrc, AssimEvent.OBJUP)
401 
402 @DispatchTarget.register
404  'DispatchTarget subclass for handling incoming JSDISCOVERY FrameSets.'
405  def dispatch(self, origaddr, frameset):
406  fstype = frameset.get_framesettype()
407  if CMAdb.debug:
408  CMAdb.log.debug("DispatchJSDISCOVERY: received [%s] FrameSet from [%s]"
409  % (FrameSetTypes.get(fstype)[0], repr(origaddr)))
410  sysname = None
411  for frame in frameset.iter():
412  frametype = frame.frametype()
413  if frametype == FrameTypes.HOSTNAME:
414  sysname = frame.getstr()
415  if frametype == FrameTypes.JSDISCOVER:
416  json = frame.getstr()
417  jsonconfig = pyConfigContext(init=json)
418  #print 'JSON received: ', json
419  if sysname is None:
420  sysname = jsonconfig.getstring('host')
421  drone = self.droneinfo.find(sysname)
422  #print >> sys.stderr, 'FOUND DRONE for %s IS: %s' % (sysname, drone)
423  #print >> sys.stderr, 'LOGGING JSON FOR DRONE for %s IS: %s' % (drone, json)
424  child = drone.find_child_system_from_json(jsonconfig)
425  #if child is not drone:
426  # print >> sys.stderr, ('>>>>>>>>>>>>>>>>>>>LOGGED child system Discovery %s: %s'
427  # % (str(child), json))
428  child.logjson(origaddr, json)
429  sysname = None
430 
431 @DispatchTarget.register
433  'DispatchTarget subclass for handling incoming SWDISCOVER FrameSets.'
434 
435  def dispatch(self, origaddr, frameset):
436  fstype = frameset.get_framesettype()
437  if CMAdb.debug:
438  CMAdb.log.debug("DispatchSWDISCOVER: received [%s] FrameSet from [%s]"
439  % (FrameSetTypes.get(fstype)[0], str(origaddr)))
440  wallclock = None
441  interface = None
442  designation = None
443  instance = None
444  for frame in frameset.iter():
445  frametype = frame.frametype()
446  if frametype == FrameTypes.HOSTNAME:
447  designation = frame.getstr()
448  elif frametype == FrameTypes.INTERFACE:
449  interface = frame.getstr()
450  elif frametype == FrameTypes.DISCNAME:
451  instance = frame.getstr()
452  elif frametype == FrameTypes.WALLCLOCK:
453  wallclock = frame.getint()
454  elif frametype == FrameTypes.PKTDATA:
455  if wallclock is None or interface is None or designation is None:
456  raise ValueError('Incomplete Switch Discovery Packet')
457  pktstart = frame.framevalue()
458  pktend = frame.frameend()
459  if instance is None:
460  instance = '_switch_%s' % interface
461  switchjson = pySwitchDiscovery.decode_discovery(designation, interface
462  , instance, wallclock, pktstart, pktend)
463  if CMAdb.debug:
464  CMAdb.log.debug('Got Link discovery info from %s: %s' \
465  % (interface, str(switchjson)))
466  drone = self.droneinfo.find(designation)
467  drone.logjson(origaddr, str(switchjson))
468  break
469 
470 @DispatchTarget.register
472  'DispatchTarget subclass for handling incoming RSCOPREPLY FrameSets.'
473  GOODTOBAD = 1
474  BADTOGOOD = 2
475  def dispatch(self, origaddr, frameset):
476  fstype = frameset.get_framesettype()
477  if CMAdb.debug:
478  CMAdb.log.debug("DispatchRSCOPREPLY: received [%s] FrameSet from [%s]"
479  % (FrameSetTypes.get(fstype)[0], str(origaddr)))
480 
481  for frame in frameset.iter():
482  frametype = frame.frametype()
483  if frametype == FrameTypes.RSCJSONREPLY:
484  obj = pyConfigContext(frame.getstr())
485  MonitorAction.logchange(origaddr, obj)
486  return
487  CMAdb.log.critical('RSCOPREPLY message from %s did not have a RSCJSONREPLY field'
488  % (str(origaddr)))
489 
490 @DispatchTarget.register
492  'Class for handling (ignoring) CONNSHUT packets'
493  def dispatch(self, origaddr, frameset):
494  origaddr = origaddr
495  frameset = frameset
def dispatch(self, origaddr, frameset)
def dispatch(self, origaddr, frameset)
def dispatch(self, origaddr, frameset)
def dispatch(self, origaddr, frameset)
def dispatch(self, origaddr, frameset)
def dispatch(self, origaddr, frameset)
def validate_source_ip(sysname, origaddr, jsobj, listenaddr)
WINEXPORT gboolean cryptcurve25519_save_public_key(const char *key_id, gpointer public_key, int keysize)
Save a public key away to disk so it&#39;s completely usable...
def dispatch(self, origaddr, frameset)
def dispatch(self, origaddr, frameset)
def dispatch(self, origaddr, frameset)
def setconfig(self, io, config)
def dispatch(self, origaddr, frameset)