batman.py 5.5 KB

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