The Assimilation Monitoring Project
 All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Groups Pages
cma/tests/cma_test.py
Go to the documentation of this file.
1 # vim: smartindent tabstop=4 shiftwidth=4 expandtab
2 #
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 _suites = ['all', 'cma']
23 import sys
24 sys.path.append("../cma")
25 sys.path.append("/usr/local/lib/python2.7/dist-packages")
26 from py2neo import neo4j
27 from testify import *
28 from testify.utils import turtle
29 
30 from frameinfo import *
31 from AssimCclasses import *
32 import gc, sys, time, collections, os
33 from cmadb import CMAdb
34 from packetlistener import PacketListener
35 from messagedispatcher import MessageDispatcher
36 from dispatchtarget import DispatchSTARTUP, DispatchHBDEAD, DispatchJSDISCOVERY, DispatchSWDISCOVER
37 from hbring import HbRing
38 from droneinfo import DroneInfo
39 import optparse
40 from cmadb import CMAdb
41 
42 
43 WorstDanglingCount = 0
44 CheckForDanglingClasses = True
45 DEBUG=True
46 DoAudit=True
47 SavePackets=True
48 doHBDEAD=True
49 MaxDrone=4
50 MaxDrone=3
51 MaxDrone=10000
52 MaxDrone=5
53 BuildListOnly = True
54 
55 t1 = MaxDrone
56 if t1 < 1000: t1 = 1000
57 t2 = MaxDrone/100
58 if t2 < 10: t2 = 10
59 t3 = t2
60 
61 if BuildListOnly:
62  doHBDEAD=False
63  SavePackets=False
64  DoAudit=False
65  CheckForDanglingClasses=False
66  DEBUG=False
67 
68 
69 
70 #gc.set_threshold(t1, t2, t3)
71 
73  global CheckForDanglingClasses
74  global WorstDanglingCount
75  CMAdb.cdb = None
76  CMAdb.io = None
77  CMAdb.TheOneRing = None
78  HbRing.ringnames = {}
79  gc.collect() # For good measure...
81  #print >>sys.stderr, "CHECKING FOR DANGLING CLASSES (%d)..." % count
82  # Avoid cluttering the output up with redundant messages...
83  if count > WorstDanglingCount and CheckForDanglingClasses:
84  WorstDanglingCount = count
86  raise AssertionError, "Dangling C-class objects - %d still around" % count
87 
88 # Values to substitute into this string via '%' operator:
89 # dronedesignation (%s) MAC address byte (%02x), MAC address byte (%02x), IP address (%s)
90 netdiscoveryformat='''
91 {
92  "discovertype": "netconfig",
93  "description": "IP Network Configuration",
94  "source": "netconfig",
95  "host": "%s",
96  "data": {
97  "eth0": {
98  "address": "00:1b:fc:1b:%02x:%02x",
99  "carrier": 1,
100  "duplex": "full",
101  "mtu": 1500,
102  "operstate": "up",
103  "speed": 1000,
104  "default_gw": true,
105  "ipaddrs": { "%s/16": {"brd":"10.20.255.255", "scope":"global", "name":"eth0"}}
106  },
107  "lo": {
108  "address": "00:00:00:00:00:00",
109  "carrier": 1,
110  "mtu": 16436,
111  "operstate": "unknown",
112  "ipaddrs": { "127.0.0.1/8": {"scope":"host"}, "::1/128": {"scope":"host"}}
113  }
114  }
115 }
116 '''
117 
118 
119 byte1 = 10
120 byte2 = 20
121 
122 def droneipaddress(hostnumber):
123  byte2 = int(hostnumber / 65536)
124  byte3 = int((hostnumber / 256) % 256)
125  byte4 = hostnumber % 256
126  return pyNetAddr([byte1,byte2,byte3,byte4],1984)
127 
128 def dronedesignation(hostnumber):
129  return 'drone%06d' % hostnumber
130 
131 def hostdiscoveryinfo(hostnumber):
132  byte3 = int(hostnumber / 256)
133  byte4 = hostnumber % 256
134  s = str(droneipaddress(hostnumber))
135  return netdiscoveryformat % (dronedesignation(hostnumber), byte3, byte4, s)
136 
137 def geninitconfig(ouraddr):
138  return {
139  'cmainit': ouraddr, # Initial 'hello' address
140  'cmaaddr': ouraddr, # not sure what this one does...
141  'cmadisc': ouraddr, # Discovery packets sent here
142  'cmafail': ouraddr, # Failure packets sent here
143  'cmaport': 1984,
144  'hbport': 1984,
145  'outsig': pySignFrame(1),
146  'deadtime': 10*1000000,
147  'warntime': 3*1000000,
148  'hbtime': 1*1000000,
149  }
150 
151 class AUDITS(TestCase):
152  def auditadrone(self, droneid):
153  designation = dronedesignation(droneid)
154  droneip = droneipaddress(droneid)
155  droneipstr = str(droneip)
156  # Did the drone get put in the DroneInfo table?
157  drone=DroneInfo.find(designation)
158  self.assertTrue(drone is not None)
159  # Did the drone's list of addresses get updated?
160  ipnodes = drone.node.get_related_nodes(neo4j.Direction.INCOMING, 'iphost')
161  self.assertEqual(len(ipnodes), 1)
162  ipnode = ipnodes[0]
163  ipnodeaddr = ipnode['name']
164  ipnodeaddrfoo = ipnodeaddr + '/16'
165  json = drone['JSON_netconfig']
166  jsobj = pyConfigContext(init=json)
167  jsdata = jsobj['data']
168  eth0obj = jsdata['eth0']
169  # Does the drone address table match the info from JSON?
170  eth0addrs = eth0obj['ipaddrs']
171  self.assertTrue(eth0addrs.has_key(ipnodeaddrfoo))
172  # Do we know that eth0 is the default gateway?
173  self.assertEqual(eth0obj['default_gw'], 1)
174 
175  # the JSON should have exactly 5 top-level keys
176  self.assertEqual(len(jsobj.keys()), 5)
177  # Was the JSON host name saved away correctly?
178  self.assertEqual(jsobj['host'], designation)
179 
180  return
181  peercount=0
182  ringcount=0
183  for ring in drone.ringmemberships.values():
184  ringcount += 1
185  # How many peers should it have?
186  if len(ring.memberlist) == 1:
187  pass # No peers in this ring...
188  elif len(ring.memberlist) == 2:
189  peercount += 1
190  else:
191  peercount += 2
192  # Make sure we're listed under our designation
193  #print >>sys.stderr, "DRONE is %s status %s" % (drone.designation, drone.status)
194  #print >>sys.stderr, "DRONE ringmemberships:", drone.ringmemberships.keys()
195  self.assertEqual(ring.members[drone.designation].designation, drone.designation)
196  self.assertEqual(len(ring.members), len(ring.memberlist))
197  if drone.status != 'dead':
198  # We have to be members of at least one ring...
199  self.assertTrue(ringcount >= 1)
200  # Drone should be a member of one ring (for now)
201  self.assertEqual(len(drone.ringmemberships),1)
202  # Do we have the right number of ring peers?
203  #print >>sys.stderr, "Checking peer count for drone %s (%d)" % (drone, len(drone.ringpeers))
204  self.assertEqual(len(drone.ringpeers), peercount)
205 
206  def auditSETCONFIG(self, packetreturn, droneid, configinit):
207  toaddr = packetreturn[0]
208  sentfs = packetreturn[1]
209  droneip = droneipaddress(droneid)
210 
211  # Was it a SETCONFIG packet?
212  self.assertEqual(sentfs.get_framesettype(), FrameSetTypes.SETCONFIG)
213  # Was the SETCONFIG sent back to the drone?
214  self.assertEqual(toaddr,droneip)
215  # Lets check the number of Frames in the SETCONFIG Frameset
216  configlen = len(configinit)-1 # We do not send Frames in configinfo
217  expectedlen = 2 * configlen + 4 # each address has a port that goes with it
218  self.assertEqual(expectedlen, len(sentfs)) # Was it the right size?
219 
220  def auditaRing(self, ringname):
221  'Verify that each ring has its neighbor pairs set up properly'
222  # Check that each element of the ring is connected to its neighbors...
223  ring = HbRing.ringnames[ringname]
224  #print "Ring %s: %s" % (ringname, str(ring))
225  listmembers = {}
226  ringmembers = {}
227  for drone in ring.members():
228  ringmembers[drone.node['name']] = None
229  for drone in ring.membersfromlist():
230  listmembers[drone.node['name']] = None
231  for drone in listmembers.keys():
232  self.assertTrue(drone in ringmembers)
233  for drone in ringmembers.keys():
234  self.assertTrue(drone in listmembers)
235 
236 
238  audit = AUDITS()
239  dronetype = CMAdb.cdb.nodetypetbl['Drone']
240  droneobjs = dronetype.get_related_nodes(neo4j.Direction.INCOMING, 'IS_A')
241  numdrones = len(droneobjs)
242  for droneid in range(0,numdrones):
243  audit.auditadrone(droneid+1)
244 
246  audit = AUDITS()
247  for ring in HbRing.ringnames:
248  audit.auditaRing(ring)
249 
250 class TestIO:
251  '''A pyNetIOudp replacement for testing. It is given a list of packets to be 'read' and in turn
252  saves all the packets it 'writes' for us to inspect.
253  '''
254  def __init__(self, addrframesetpairs, sleepatend=0):
255  if isinstance(addrframesetpairs, tuple):
256  addrframesetpairs = addrframesetpairs
257  self.inframes = addrframesetpairs
259  self.packetsread=0
260  self.sleepatend=sleepatend
261  self.index=0
262  self.writecount=0
263  self.config = {CONFIGNAME_CMAPORT: 1984}
264 
265  def recvframesets(self):
266  # Audit after each packet is processed - and once before the first packet.
267  if DoAudit:
268  if self.packetsread < 200 or (self.packetsread % 500) == 0:
270  auditallrings()
271  if self.index >= len(self.inframes):
272  time.sleep(self.sleepatend)
273  raise StopIteration('End of Packets')
274  ret = self.inframes[self.index]
275  self.index += 1
276  self.packetsread += len(ret[1])
277  return ret
278 
279  def sendframesets(self, dest, fslist):
280  if not isinstance(fslist, collections.Sequence):
281  return self._sendaframeset(dest, fslist)
282  for fs in fslist:
283  self._sendaframeset(dest, fs)
284 
285  def sendreliablefs(self, dest, fslist):
286  self.sendframesets(dest, fslist)
287 
288  def ackmessage(self, dest, fs):
289  pass
290 
291  def closeconn(self, qid, dest):
292  pass
293 
294  def _sendaframeset(self, dest, fslist):
295  self.writecount += 1
296  if SavePackets:
297  self.packetswritten.append((dest,fslist))
298 
299  def getmaxpktsize(self): return 60000
300  def getfd(self): return 4
301  def bindaddr(self, addr): return True
302  def mcastjoin(self, addr): return True
303  def setblockio(self, tf): return
304 
305  def dumppackets(self):
306  print >>sys.stderr, 'Sent %d packets' % len(self.packetswritten)
307  for packet in self.packetswritten:
308  print '%s (%s)' % (packet[0], packet[1])
309 
310 
311 class TestTestInfrastructure(TestCase):
312  def test_eof(self):
313  'Get EOF with empty input'
314  if BuildListOnly: return
315  framesets=[]
316  io = TestIO(framesets, 0)
317  CMAdb.initglobal(io, True)
318  # just make sure it seems to do the right thing
319  self.assertRaises(StopIteration, io.recvframesets)
321 
322  def test_get1pkt(self):
323  'Read a single packet'
324  if BuildListOnly: return
325  otherguy = pyNetAddr([1,2,3,4],)
326  strframe1=pyCstringFrame(FrameTypes.CSTRINGVAL, "Hello, world.")
327  fs = pyFrameSet(42)
328  fs.append(strframe1)
329  framesets=((otherguy, (strframe1,)),)
330  io = TestIO(framesets, 0)
331  CMAdb.initglobal(io, True)
332  gottenfs = io.recvframesets()
333  self.assertEqual(len(gottenfs), 2)
334  self.assertEqual(gottenfs, framesets[0])
335  self.assertRaises(StopIteration, io.recvframesets)
336 
337  def test_echo1pkt(self):
338  'Read a packet and write it back out'
339  if BuildListOnly: return
340  strframe1=pyCstringFrame(FrameTypes.CSTRINGVAL, "Hello, world.")
341  fs = pyFrameSet(42)
342  fs.append(strframe1)
343  otherguy = pyNetAddr([1,2,3,4],)
344  framesets=((otherguy, (strframe1,)),)
345  io = TestIO(framesets, 0)
346  CMAdb.initglobal(io, True)
347  fslist = io.recvframesets() # read in a packet
348  self.assertEqual(len(fslist), 2)
349  self.assertEqual(fslist, framesets[0])
350  io.sendframesets(fslist[0], fslist[1]) # echo it back out
351  self.assertEqual(len(io.packetswritten), 1)
352  self.assertEqual(len(io.packetswritten), len(framesets))
353  self.assertRaises(StopIteration, io.recvframesets)
354 
355  @class_teardown
356  def tearDown(self):
358 
359 class TestCMABasic(TestCase):
360  def test_startup(self):
361  '''A semi-interesting test: We send a STARTUP message and get back a
362  SETCONFIG message with lots of good stuff in it.'''
363  if BuildListOnly: return
364  droneid = 1
365  droneip = droneipaddress(droneid)
366  designation = dronedesignation(droneid)
367  designationframe=pyCstringFrame(FrameTypes.HOSTNAME, designation)
368  dronediscovery=hostdiscoveryinfo(droneid)
369  discoveryframe=pyCstringFrame(FrameTypes.JSDISCOVER, dronediscovery)
370  fs = pyFrameSet(FrameSetTypes.STARTUP)
371  fs.append(designationframe)
372  fs.append(discoveryframe)
373  fsin = ((droneip, (fs,)),)
374  io = TestIO(fsin,0)
375  CMAdb.initglobal(io, True)
376  OurAddr = pyNetAddr((127,0,0,1),1984)
377  disp = MessageDispatcher({FrameSetTypes.STARTUP: DispatchSTARTUP()})
378  configinit = geninitconfig(OurAddr)
379  config = pyConfigContext(init=configinit)
380  listener = PacketListener(config, disp, io=io)
381  # We send the CMA an intial STARTUP packet
382  self.assertRaises(StopIteration, listener.listen) # We audit after each packet is processed
383  # Let's see what happened...
384 
385  self.assertEqual(len(io.packetswritten), 2) # Did we send out two packets?
386  # Note that this change over time
387  # As we change discovery...
388  AUDITS().auditSETCONFIG(io.packetswritten[0], droneid, configinit)
389  # Drone and Ring tables are automatically audited after each packet
390 
392  '''A very interesting test: We send a STARTUP message and get back a
393  SETCONFIG message and then send back a bunch of discovery requests.'''
394  OurAddr = pyNetAddr((10,10,10,5), 1984)
395  configinit = geninitconfig(OurAddr)
396  fsin = []
397  droneid=0
398  for droneid in range(1,MaxDrone+1):
399  droneip = droneipaddress(droneid)
400  designation = dronedesignation(droneid)
401  designationframe=pyCstringFrame(FrameTypes.HOSTNAME, designation)
402  dronediscovery=hostdiscoveryinfo(droneid)
403  discoveryframe=pyCstringFrame(FrameTypes.JSDISCOVER, dronediscovery)
404  fs = pyFrameSet(FrameSetTypes.STARTUP)
405  fs.append(designationframe)
406  fs.append(discoveryframe)
407  fsin.append((droneip, (fs,)))
408  addrone = droneipaddress(1)
409  maxdrones = droneid
410  if doHBDEAD:
411  for droneid in range(2,maxdrones+1):
412  droneip = droneipaddress(droneid)
413  deadframe=pyIpPortFrame(FrameTypes.IPPORT, addrstring=droneip)
414  fs = pyFrameSet(FrameSetTypes.HBDEAD)
415  fs.append(deadframe)
416  fsin.append((addrone, (fs,)))
417  io = TestIO(fsin)
418  CMAdb.initglobal(io, True)
419  disp = MessageDispatcher( {
420  FrameSetTypes.STARTUP: DispatchSTARTUP(),
421  FrameSetTypes.HBDEAD: DispatchHBDEAD(),
422  })
423  config = pyConfigContext(init=configinit)
424  listener = PacketListener(config, disp, io=io)
425  # We send the CMA a BUNCH of intial STARTUP packets
426  try:
427  listener.listen()
428  except StopIteration as foo:
429  pass
430  #self.assertRaises(StopIteration, listener.listen)
431  # We audit after each packet is processed
432  # The auditing code will make sure all is well...
433  # But it doesn't know how many drones we just registered
434  droneroot = CMAdb.cdb.nodetypetbl['Drone']
435  Dronerels = droneroot.get_relationships(neo4j.Direction.INCOMING, 'IS_A')
436  self.assertEqual(len(Dronerels), maxdrones)
437  if doHBDEAD:
438  partnercount = 0
439  livecount = 0
440  ringcount = 0
441  for dronerel in Dronerels:
442  drone1 = DroneInfo(dronerel.start_node)
443  if drone1.node['status'] != 'dead': livecount += 1
444  drone1rels = drone1.node.get_relationships()
445  for rel in drone1rels:
446  reltype = rel.type
447  if reltype.startswith(HbRing.memberprefix):
448  ringcount += 1
449  if reltype.startswith(HbRing.nextprefix):
450  partnercount += 1
451  self.assertEqual(partnercount, 0)
452  self.assertEqual(livecount, 1)
453  self.assertEqual(ringcount, 1)
454  if DoAudit:
456  auditallrings()
457 
458  print "The CMA read %d packets." % io.packetsread
459  print "The CMA wrote %d packets." % io.writecount
460  #io.dumppackets()
461 
462 
463  @class_teardown
464  def tearDown(self):
466 
467 if __name__ == "__main__":
468  run()