batman.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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()[2:]:
  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. # call batadv-vis and parse output as unicode JSON
  65. rawdata = subprocess.check_output([self.batadv_vis, '-f', 'jsondoc'])
  66. rawdata = unicode(rawdata, encoding='latin1', errors='replace')
  67. batmandata = json.loads(rawdata)
  68. # parse vis data
  69. for item in visdata:
  70. itemmac = item.get('primary', item.get('router'))
  71. itemid = ffstatus.mac2id(itemmac)
  72. content = {
  73. 'aliases': [],
  74. 'neighbours': {},
  75. '__UPDATED__': {'batctl': timestamp},
  76. }
  77. neigh_id = ffstatus.mac2id(item.get('neighbor'))
  78. if neigh_id is not None:
  79. neigh_q = item.get('label', '0')
  80. if neigh_q != 'TT':
  81. neigh_q = float(neigh_q)
  82. else:
  83. neigh_q = None
  84. content['neighbours'][neigh_id] = neigh_q
  85. if itemid is None:
  86. # assumption: this is a secondary
  87. secid = ffstatus.mac2id(item.get('secondary'))
  88. ofid = ffstatus.mac2id(item.get('of'))
  89. if secid is None or ofid is None:
  90. print('BATCTL-VD threw garbage: ' + json.dumps(item))
  91. continue
  92. itemid = ofid
  93. content['aliases'].append(secid)
  94. else:
  95. if not 'mac' in content:
  96. content['mac'] = itemmac
  97. if itemid in gateways:
  98. content['type'] = 'gateway'
  99. if itemid in data:
  100. data[itemid] = ffstatus.dict_merge(
  101. data[itemid], content, overwrite_lists=False)
  102. else:
  103. data[itemid] = content
  104. # parse raw data, convert all MAC into nodeid
  105. for item in batmandata['vis']:
  106. itemid = ffstatus.mac2id(item['primary'])
  107. aliases = []
  108. if 'secondary' in item:
  109. for mac in item['secondary']:
  110. aliases.append(ffstatus.mac2id(mac))
  111. neighbours = {}
  112. if 'neighbors' in item:
  113. for neighbour in item['neighbors']:
  114. #if neighbour['router'] != item['primary']:
  115. # print('node {0}\'s neighbor {1} has unexpected router {2}'.format(itemid, neighbour['neighbor'], neighbour['router']))
  116. metric = float(neighbour['metric'])
  117. neighbours[neighbour['neighbor']] = metric
  118. # construct dict entry as expected by BATCAVE
  119. content = {
  120. 'aliases': aliases,
  121. 'neighbours': neighbours,
  122. 'clients': [x for x in item.get('clients', [])],
  123. '__UPDATED__': {'batadv': timestamp},
  124. '__RAW__': {'batadv': {itemid: item}},
  125. }
  126. if itemid in data:
  127. data[itemid] = ffstatus.dict_merge(
  128. data[itemid], content, overwrite_lists=False)
  129. else:
  130. data[itemid] = content
  131. return data
  132. # standalone test mode
  133. if __name__ == "__main__":
  134. parser = BatmanParser()
  135. try:
  136. parser.sanitycheck()
  137. except SanityCheckError as err:
  138. print('SANITY-CHECK failed:', str(err))
  139. import sys
  140. sys.exit(1)
  141. bdata = parser.fetch()
  142. print(json.dumps(bdata))