batman.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. from __future__ import print_function
  4. import io
  5. import json
  6. import re
  7. import string
  8. import subprocess
  9. import time
  10. import ffstatus
  11. from .exceptions import SanityCheckError
  12. class BatmanParser:
  13. batadv_vis = 'batadv-vis'
  14. batctl = 'batctl'
  15. mactranslation = string.maketrans('2367abef', '014589cd')
  16. def __str__(self):
  17. return 'BatmanParser using \'{0}\' and \'{1}\''.format(
  18. self.batadv_vis, self.batctl)
  19. def sanitycheck(self):
  20. """Checks that programs are executable and give sane output."""
  21. testdata = None
  22. try:
  23. cmd = [self.batadv_vis, '-f', 'jsondoc']
  24. testdata = subprocess.check_output(cmd)
  25. except Exception as err:
  26. raise SanityCheckError(
  27. self, "batadv-vis not found or incompatible", err)
  28. try:
  29. json.loads(testdata)
  30. except Exception as err:
  31. raise SanityCheckError(
  32. self, "batadv-vis does not return valid JSON data", err)
  33. try:
  34. cmd = [self.batctl, 'vd', 'json', '-n']
  35. testdata = subprocess.check_output(cmd)
  36. except Exception as err:
  37. raise SanityCheckError(
  38. self, "batctl not found or incompatible", err)
  39. lineno = 0
  40. try:
  41. for line in testdata.splitlines():
  42. lineno += 1
  43. json.loads(line)
  44. except Exception as err:
  45. raise SanityCheckError(
  46. self,
  47. "batctl does not return valid JSON data (line " + lineno + ")",
  48. err)
  49. return True
  50. def fetch(self):
  51. """Fetches the current data from batadv-vis."""
  52. data = {}
  53. timestamp = int(time.time())
  54. # call 'batctl vd json -n' and parse output line-wise as JSON
  55. rawvisdata = subprocess.check_output([self.batctl, 'vd', 'json', '-n'])
  56. visdata = [json.loads(line) for line in rawvisdata.splitlines()]
  57. # call 'batctl gwl' and parse output lines
  58. rawgwdata = subprocess.check_output([self.batctl, 'gwl'])
  59. gateways = []
  60. for l in rawgwdata.splitlines()[1:]:
  61. m = re.search("^\s*(([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2})\s+", l)
  62. if m is not None:
  63. gateways.append(ffstatus.mac2id(m.group(1)))
  64. else:
  65. print('Failed to parse gateway from batctl-gwl line:', l)
  66. # call batadv-vis and parse output as unicode JSON
  67. rawdata = subprocess.check_output([self.batadv_vis, '-f', 'jsondoc'])
  68. rawdata = unicode(rawdata, encoding='latin1', errors='replace')
  69. batmandata = json.loads(rawdata)
  70. # parse vis data
  71. for item in visdata:
  72. itemmac = item.get('primary', item.get('router'))
  73. itemid = ffstatus.mac2id(itemmac)
  74. content = {
  75. 'aliases': [],
  76. 'neighbours': {},
  77. '__UPDATED__': {'batctl': timestamp},
  78. '__RAW__': {'batctl': [item]},
  79. }
  80. neigh_id = ffstatus.mac2id(item.get('neighbor'))
  81. if neigh_id is not None:
  82. neigh_q = item.get('label', '0')
  83. if neigh_q != 'TT':
  84. neigh_q = float(neigh_q)
  85. else:
  86. neigh_q = None
  87. content['neighbours'][neigh_id] = neigh_q
  88. if itemid is None:
  89. # assumption: this is a secondary
  90. secid = ffstatus.mac2id(item.get('secondary'))
  91. ofid = ffstatus.mac2id(item.get('of'))
  92. if secid is None or ofid is None:
  93. print('BATCTL-VD threw garbage: ' + json.dumps(item))
  94. continue
  95. itemid = ofid
  96. content['aliases'].append(secid)
  97. else:
  98. if not 'mac' in content:
  99. content['mac'] = itemmac
  100. if itemid in gateways:
  101. content['type'] = 'gateway'
  102. if itemid in data:
  103. data[itemid] = ffstatus.dict_merge(
  104. data[itemid], content, overwrite_lists=False)
  105. else:
  106. data[itemid] = content
  107. # parse raw data, convert all MAC into nodeid
  108. for item in batmandata['vis']:
  109. itemid = ffstatus.mac2id(item['primary'])
  110. aliases = []
  111. if 'secondary' in item:
  112. for mac in item['secondary']:
  113. aliases.append(ffstatus.mac2id(mac))
  114. neighbours = {}
  115. if 'neighbors' in item:
  116. for neighbour in item['neighbors']:
  117. #if neighbour['router'] != item['primary']:
  118. # print('node {0}\'s neighbor {1} has unexpected router {2}'.format(itemid, neighbour['neighbor'], neighbour['router']))
  119. metric = float(neighbour['metric'])
  120. neighbours[neighbour['neighbor']] = metric
  121. # construct dict entry as expected by BATCAVE
  122. content = {
  123. 'aliases': aliases,
  124. 'neighbours': neighbours,
  125. 'clients': [x for x in item.get('clients', [])],
  126. '__UPDATED__': {'batadv': timestamp},
  127. '__RAW__': {'batadv': {itemid: item}},
  128. }
  129. if itemid in data:
  130. data[itemid] = ffstatus.dict_merge(
  131. data[itemid], content, overwrite_lists=False)
  132. else:
  133. data[itemid] = content
  134. return data
  135. # standalone test mode
  136. if __name__ == "__main__":
  137. parser = BatmanParser()
  138. try:
  139. parser.sanitycheck()
  140. except SanityCheckError as err:
  141. print('SANITY-CHECK failed:', str(err))
  142. import sys
  143. sys.exit(1)
  144. bdata = parser.fetch()
  145. print(json.dumps(bdata))