ext-respondd.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. #!/usr/bin/env python3
  2. # Code-Base: https://github.com/ffggrz/ffnord-alfred-announce
  3. # + https://github.com/freifunk-mwu/ffnord-alfred-announce
  4. # + https://github.com/FreifunkBremen/respondd
  5. import json
  6. import socket
  7. import subprocess
  8. import re
  9. import netifaces as netif
  10. from cpuinfo import cpuinfo
  11. # Force encoding to UTF-8
  12. import locale # Ensures that subsequent open()s
  13. locale.getpreferredencoding = lambda _=None: 'UTF-8' # are UTF-8 encoded.
  14. import sys
  15. import struct
  16. import select
  17. import zlib
  18. def toUTF8(line):
  19. return line.decode("utf-8")
  20. def call(cmdnargs):
  21. output = subprocess.check_output(cmdnargs)
  22. lines = output.splitlines()
  23. lines = [toUTF8(line) for line in lines]
  24. return lines
  25. def merge(a, b):
  26. if isinstance(a, dict) and isinstance(b, dict):
  27. d = dict(a)
  28. d.update({k: merge(a.get(k, None), b[k]) for k in b})
  29. return d
  30. if isinstance(a, list) and isinstance(b, list):
  31. return [merge(x, y) for x, y in itertools.izip_longest(a, b)]
  32. return a if b is None else b
  33. def getGateway(batadv_dev):
  34. #/sys/kernel/debug/batman_adv/bat0/gateways
  35. output = subprocess.check_output(["batctl","-m",batadv_dev,"gwl","-n"])
  36. output_utf8 = output.decode("utf-8")
  37. lines = output_utf8.splitlines()
  38. gw = None
  39. for line in lines:
  40. gw_line = re.match(r"^=> +([0-9a-f:]+) ", line)
  41. if gw_line:
  42. gw = gw_line.group(1)
  43. return gw
  44. def getClients(batadv_dev):
  45. #/sys/kernel/debug/batman_adv/bat0/transtable_local
  46. output = subprocess.check_output(["batctl","-m",batadv_dev,"tl","-n"])
  47. output_utf8 = output.decode("utf-8")
  48. lines = output_utf8.splitlines()
  49. j = {"total": 0, "wifi": 0}
  50. for line in lines:
  51. # batman-adv -> translation-table.c -> batadv_tt_local_seq_print_text
  52. # R = BATADV_TT_CLIENT_ROAM
  53. # P = BATADV_TT_CLIENT_NOPURGE
  54. # N = BATADV_TT_CLIENT_NEW
  55. # X = BATADV_TT_CLIENT_PENDING
  56. # W = BATADV_TT_CLIENT_WIFI
  57. # I = BATADV_TT_CLIENT_ISOLA
  58. # . = unset
  59. # * c0:11:73:b2:8f:dd -1 [.P..W.] 1.710 (0xe680a836)
  60. ml = re.match(r"^\s\*\s([0-9a-f:]+)\s+-\d\s\[([RPNXWI\.]+)\]", line, re.I)
  61. if ml:
  62. j["total"] += 1
  63. if ml.group(2)[4] == 'W':
  64. j["wifi"] += 1
  65. return j
  66. def getAddresses(bridge_dev):
  67. ip_addrs = netif.ifaddresses(bridge_dev)
  68. ip_list = []
  69. try:
  70. for ip6 in netif.ifaddresses(bridge_dev)[netif.AF_INET6]:
  71. raw6 = ip6['addr'].split('%')
  72. ip_list.append(raw6[0])
  73. except:
  74. pass
  75. return ip_list
  76. def getMac_mesh(fastd_dev,meshmode=False):
  77. interface = netif.ifaddresses(fastd_dev)
  78. mesh = []
  79. mac = None
  80. try:
  81. mac = interface[netif.AF_LINK]
  82. mesh.append(mac[0]['addr'])
  83. except:
  84. KeyError
  85. if meshmode:
  86. return mesh
  87. else:
  88. return mac[0]['addr']
  89. def getMesh_interfaces(batadv_dev):
  90. output = subprocess.check_output(["batctl","-m",batadv_dev,"if"])
  91. output_utf8 = output.decode("utf-8")
  92. lines = output_utf8.splitlines()
  93. mesh = []
  94. for line in lines:
  95. dev_line = re.match(r"^([^:]*)", line)
  96. interface = netif.ifaddresses(dev_line.group(0))
  97. mac = interface[netif.AF_LINK]
  98. mesh.append(mac[0]['addr'])
  99. return mesh
  100. def getBat0_mesh(batadv_dev):
  101. output = subprocess.check_output(["batctl","-m",batadv_dev,"if"])
  102. output_utf8 = output.decode("utf-8")
  103. lines = output_utf8.splitlines()
  104. j = {"tunnel" : []}
  105. for line in lines:
  106. dev_line = re.match(r"^([^:]*)", line)
  107. nif = dev_line.group(0)
  108. interface = netif.ifaddresses(nif)
  109. mac = interface[netif.AF_LINK]
  110. j["tunnel"].append(mac[0]['addr'])
  111. return j
  112. def getTraffic(batadv_dev):
  113. return (lambda fields:
  114. dict(
  115. (key, dict(
  116. (type_, int(value_))
  117. for key_, type_, value_ in fields
  118. if key_ == key))
  119. for key in ['rx', 'tx', 'forward', 'mgmt_rx', 'mgmt_tx']
  120. )
  121. )(list(
  122. (
  123. key.replace('_bytes', '').replace('_dropped', ''),
  124. 'bytes' if key.endswith('_bytes') else 'dropped' if key.endswith('_dropped') else 'packets',
  125. value
  126. )
  127. for key, value in map(lambda s: list(map(str.strip, s.split(': ', 1))), call(['ethtool', '-S', batadv_dev])[1:])
  128. ))
  129. def getMemory():
  130. return dict(
  131. (key.replace('Mem', '').lower(), int(value.split(' ')[0]))
  132. for key, value in map(lambda s: map(str.strip, s.split(': ', 1)), open('/proc/meminfo').readlines())
  133. if key in ('MemTotal', 'MemFree', 'Buffers', 'Cached')
  134. )
  135. def getFastd(fastd_socket): # Unused
  136. fastd_data = b""
  137. try:
  138. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  139. sock.connect(fastd_socket)
  140. except socket.error as err:
  141. print("socket error: ", sys.stderr, err)
  142. sys.exit(1)
  143. while True:
  144. data = sock.recv(1024)
  145. if not data: break
  146. fastd_data+= data
  147. sock.close()
  148. return json.loads(fastd_data.decode("utf-8"))
  149. def getNode_id(dev):
  150. if 'node_id' in aliases["nodeinfo"]:
  151. return aliases["nodeinfo"]["node_id"]
  152. else:
  153. return mac_mesh(dev).replace(':','')
  154. def getNeighbours():
  155. # https://github.com/freifunk-gluon/packages/blob/master/net/respondd/src/respondd.c
  156. # wenn Originators.mac == next_hop.mac dann
  157. j = {}
  158. with open("/sys/kernel/debug/batman_adv/" + batadv_dev + "/originators", 'r') as fh:
  159. for line in fh:
  160. #62:e7:27:cd:57:78 0.496s (192) de:ad:be:ef:01:01 [ mesh-vpn]: de:ad:be:ef:01:01 (192) de:ad:be:ef:02:01 (148) de:ad:be:ef:03:01 (148)
  161. ml = re.match(r"^([0-9a-f:]+)[ ]*([\d\.]*)s[ ]*\((\d*)\)[ ]*([0-9a-f:]+)[ ]*\[[ ]*(.*)\]", line, re.I)
  162. if ml:
  163. if ml.group(1) == ml.group(4):
  164. j[ml.group(1)] = {
  165. "tq": int(ml.group(3)),
  166. "lastseen": float(ml.group(2)),
  167. }
  168. return j
  169. # ======================== Output =========================
  170. # =========================================================
  171. def createNodeinfo():
  172. j = {
  173. "node_id": getNode_id(fastd_dev),
  174. "hostname": socket.gethostname(),
  175. "network": {
  176. "addresses": getAddresses(bridge_dev),
  177. "mesh": {
  178. "bat0": {
  179. "interfaces": getBat0_mesh(batadv_dev),
  180. },
  181. },
  182. "mac": getMac_mesh(fastd_dev),
  183. "mesh_interfaces": getMesh_interfaces(batadv_dev),
  184. },
  185. "software": {
  186. "firmware": {
  187. "base": call(['lsb_release','-is'])[0],
  188. "release": call(['lsb_release','-ds'])[0],
  189. },
  190. "batman-adv": {
  191. "version": open('/sys/module/batman_adv/version').read().strip(),
  192. # "compat": # /lib/gluon/mesh-batman-adv-core/compat
  193. },
  194. "fastd": {
  195. "version": call(['fastd','-v'])[0].split(' ')[1],
  196. "enabled": True,
  197. },
  198. "status-page": {
  199. "api": 0,
  200. },
  201. "autoupdater": {
  202. # "branch": "stable",
  203. "enabled": False,
  204. },
  205. },
  206. "hardware": {
  207. "model": cpuinfo.get_cpu_info()["brand"],
  208. "nproc": int(call(['nproc'])[0]),
  209. },
  210. # "vpn": True,
  211. "owner": {},
  212. "system": {},
  213. "location": {},
  214. }
  215. return merge(j, aliases["nodeinfo"])
  216. def createStatistics():
  217. j = {
  218. "node_id": getNode_id(fastd_dev),
  219. "gateway" : getGateway(batadv_dev), # BUG: wenn man ein Gateway ist, was soll man dann hier senden?
  220. "clients": getClients(batadv_dev),
  221. "traffic": getTraffic(batadv_dev),
  222. "idletime": float(open('/proc/uptime').read().split(' ')[1]),
  223. "loadavg": float(open('/proc/loadavg').read().split(' ')[0]),
  224. "memory": getMemory(),
  225. "processes": dict(zip(('running', 'total'), map(int, open('/proc/loadavg').read().split(' ')[3].split('/')))),
  226. "uptime": float(open('/proc/uptime').read().split(' ')[0]),
  227. # "mesh_vpn": { # getFastd
  228. # "groups": {
  229. # "backbone": {
  230. # "peers": {
  231. # "vpn1": None,
  232. # "vpn2": {
  233. # "established": 1000,
  234. # },
  235. # "vpn3": None,
  236. # },
  237. # },
  238. # },
  239. # },
  240. }
  241. return j
  242. def createNeighbours():
  243. #/sys/kernel/debug/batman_adv/bat0/originators
  244. j = {
  245. "node_id": getNode_id(fastd_dev),
  246. "batadv": { # Testing
  247. getMac_mesh(fastd_dev): {
  248. "neighbours": getNeighbours(),
  249. },
  250. #"wifi": {},
  251. },
  252. }
  253. return j
  254. def sendResponse(request, compress):
  255. json_data = {}
  256. #https://github.com/freifunk-gluon/packages/blob/master/net/respondd/src/respondd.c
  257. if request == 'statistics':
  258. json_data[request] = createStatistics()
  259. elif request == 'nodeinfo':
  260. json_data[request] = createNodeinfo()
  261. elif request == 'neighbours':
  262. json_data[request] = createNeighbours()
  263. else:
  264. print("unknown command: " + request)
  265. return
  266. json_str = bytes(json.dumps(json_data, separators=(',', ':')), 'UTF-8')
  267. if compress:
  268. encoder = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15) # The data may be decompressed using zlib and many zlib bindings using -15 as the window size parameter.
  269. gzip_data = encoder.compress(json_str)
  270. gzip_data = gzip_data + encoder.flush()
  271. sock.sendto(gzip_data, sender)
  272. else:
  273. sock.sendto(json_str, sender)
  274. print(json.dumps(json_data, sort_keys=True, indent=4))
  275. # ===================== Mainfunction ======================
  276. # =========================================================
  277. config = {}
  278. try:
  279. with open("config.json", 'r') as cfg_handle:
  280. config = json.load(cfg_handle)
  281. except IOError:
  282. raise
  283. aliases = {}
  284. try:
  285. with open("alias.json", 'r') as cfg_handle:
  286. aliases = json.load(cfg_handle)
  287. except IOError:
  288. raise
  289. batadv_dev = config['batman']
  290. fastd_dev = config['fastd']
  291. bridge_dev = config['bridge']
  292. #print(json.dumps(getFastd(config["fastd_socket"]), sort_keys=True, indent=4))
  293. #print(json.dumps(createNodeinfo(), sort_keys=True, indent=4))
  294. #print(json.dumps(createStatistics(), sort_keys=True, indent=4))
  295. #print(json.dumps(createNeighbours(), sort_keys=True, indent=4))
  296. #print(merge(createNodeinfo(), aliases["nodeinfo"]))
  297. #print(createStatistics())
  298. #sys.exit(1)
  299. if 'addr' in config:
  300. addr = config['addr']
  301. else:
  302. addr = 'ff02::2'
  303. if 'addr' in config:
  304. port = config['port']
  305. else:
  306. port = 1001
  307. if_idx = socket.if_nametoindex(config["bridge"])
  308. group = socket.inet_pton(socket.AF_INET6, addr) + struct.pack("I", if_idx)
  309. sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
  310. sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, group)
  311. sock.bind(('::', port))
  312. # =========================================================
  313. while True:
  314. if select.select([sock],[],[],1)[0]:
  315. msg, sender = sock.recvfrom(2048) # buffer > mtu !?!? -> egal, da eh nur ein GET kommt welches kleiner ist als 1024 ist
  316. # try:
  317. # msg = zlib.decompress(msg, -15) # The data may be decompressed using zlib and many zlib bindings using -15 as the window size parameter.
  318. # except zlib.error:
  319. # pass
  320. print(msg)
  321. msg_spl = str(msg, 'UTF-8').split(" ")
  322. # BUG: Es koennen auch Anfragen wie "GET statistics nodeinfo" existieren (laut gluon doku)
  323. if msg_spl[0] == 'GET': # multi_request
  324. for request in msg_spl[1:]:
  325. sendResponse(request, True)
  326. else: # single_request
  327. sendResponse(msg_spl[0], False)