ext-respondd.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  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 sys
  6. import socket
  7. import select
  8. import struct
  9. import subprocess
  10. import argparse
  11. import re
  12. # Force encoding to UTF-8
  13. import locale # Ensures that subsequent open()s
  14. locale.getpreferredencoding = lambda _=None: 'UTF-8' # are UTF-8 encoded.
  15. import json
  16. import zlib
  17. import netifaces as netif
  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():
  34. #/sys/kernel/debug/batman_adv/bat0/gateways
  35. output = subprocess.check_output(["batctl","-m",config['batman'],"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():
  45. #/sys/kernel/debug/batman_adv/bat0/transtable_local
  46. output = subprocess.check_output(["batctl","-m",config['batman'],"tl","-n"])
  47. output_utf8 = output.decode("utf-8")
  48. lines = output_utf8.splitlines()
  49. batadv_mac = getDevice_MAC(config['batman'])
  50. j = {"total": 0, "wifi": 0}
  51. for line in lines:
  52. # batman-adv -> translation-table.c -> batadv_tt_local_seq_print_text
  53. # R = BATADV_TT_CLIENT_ROAM
  54. # P = BATADV_TT_CLIENT_NOPURGE
  55. # N = BATADV_TT_CLIENT_NEW
  56. # X = BATADV_TT_CLIENT_PENDING
  57. # W = BATADV_TT_CLIENT_WIFI
  58. # I = BATADV_TT_CLIENT_ISOLA
  59. # . = unset
  60. # * c0:11:73:b2:8f:dd -1 [.P..W.] 1.710 (0xe680a836)
  61. ml = re.match(r"^\s\*\s([0-9a-f:]+)\s+-\d\s\[([RPNXWI\.]+)\]", line, re.I)
  62. if ml:
  63. if not batadv_mac == ml.group(1): # Filter bat0
  64. if not ml.group(1).startswith('33:33:') and not ml.group(1).startswith('01:00:5e:'): # Filter Multicast
  65. j["total"] += 1
  66. if ml.group(2)[4] == 'W':
  67. j["wifi"] += 1
  68. return j
  69. def getDevice_Addresses(dev):
  70. l = []
  71. try:
  72. for ip6 in netif.ifaddresses(dev)[netif.AF_INET6]:
  73. raw6 = ip6['addr'].split('%')
  74. l.append(raw6[0])
  75. except:
  76. pass
  77. return l
  78. def getDevice_MAC(dev):
  79. try:
  80. interface = netif.ifaddresses(dev)
  81. mac = interface[netif.AF_LINK]
  82. return mac[0]['addr']
  83. except:
  84. return None
  85. def getMesh_Interfaces():
  86. j = {}
  87. output = subprocess.check_output(["batctl","-m",config['batman'],"if"])
  88. output_utf8 = output.decode("utf-8")
  89. lines = output_utf8.splitlines()
  90. for line in lines:
  91. dev_re = re.match(r"^([^:]*)", line)
  92. dev = dev_re.group(1)
  93. j[dev] = getDevice_MAC(dev)
  94. return j
  95. def getBat0_Interfaces():
  96. j = {}
  97. output = subprocess.check_output(["batctl","-m",config['batman'],"if"])
  98. output_utf8 = output.decode("utf-8")
  99. lines = output_utf8.splitlines()
  100. for line in lines:
  101. dev_line = re.match(r"^([^:]*)", line)
  102. nif = dev_line.group(0)
  103. if_group = ""
  104. if "fastd" in config and nif == config["fastd"]: # keep for compatibility
  105. if_group = "tunnel"
  106. elif nif.find("l2tp") != -1:
  107. if_group = "l2tp"
  108. elif ("mesh-vpn" in config and nif in config["mesh-vpn"]):
  109. if_group = "tunnel"
  110. elif "mesh-wlan" in config and nif in config["mesh-wlan"]:
  111. if_group = "wireless"
  112. else:
  113. if_group = "other"
  114. if not if_group in j:
  115. j[if_group] = []
  116. j[if_group].append(getDevice_MAC(nif))
  117. return j
  118. def getTraffic(): # BUG: falsches interfaces?
  119. return (lambda fields:
  120. dict(
  121. (key, dict(
  122. (type_, int(value_))
  123. for key_, type_, value_ in fields
  124. if key_ == key))
  125. for key in ['rx', 'tx', 'forward', 'mgmt_rx', 'mgmt_tx']
  126. )
  127. )(list(
  128. (
  129. key.replace('_bytes', '').replace('_dropped', ''),
  130. 'bytes' if key.endswith('_bytes') else 'dropped' if key.endswith('_dropped') else 'packets',
  131. value
  132. )
  133. for key, value in map(lambda s: list(map(str.strip, s.split(': ', 1))), call(['ethtool', '-S', config['batman']])[1:])
  134. ))
  135. def getMemory():
  136. return dict(
  137. (key.replace('Mem', '').lower(), int(value.split(' ')[0]))
  138. for key, value in map(lambda s: map(str.strip, s.split(': ', 1)), open('/proc/meminfo').readlines())
  139. if key in ('MemTotal', 'MemFree', 'Buffers', 'Cached')
  140. )
  141. def getFastd():
  142. fastd_data = b""
  143. try:
  144. sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  145. sock.connect(config["fastd_socket"])
  146. except socket.error as err:
  147. print("socket error: ", sys.stderr, err)
  148. return None
  149. while True:
  150. data = sock.recv(1024)
  151. if not data: break
  152. fastd_data+= data
  153. sock.close()
  154. return json.loads(fastd_data.decode("utf-8"))
  155. def getMeshVPNPeers():
  156. j = {}
  157. if "fastd_socket" in config:
  158. fastd = getFastd()
  159. for peer, v in fastd["peers"].items():
  160. if v["connection"]:
  161. j[v["name"]] = {
  162. "established": v["connection"]["established"],
  163. }
  164. else:
  165. j[v["name"]] = None
  166. return j
  167. else:
  168. return None
  169. def getNode_ID():
  170. if 'node_id' in config['nodeinfo']:
  171. return config['nodeinfo']['node_id']
  172. elif 'network' in config['nodeinfo'] and 'mac' in config['nodeinfo']['network']:
  173. return config['nodeinfo']['network']['mac'].replace(':','')
  174. else:
  175. return getDevice_MAC(config["batman"]).replace(':','')
  176. def getStationDump(dev_list):
  177. j = {}
  178. for dev in dev_list:
  179. try:
  180. # iw dev ibss3 station dump
  181. output = subprocess.check_output(["iw","dev",dev,"station", "dump"], stderr=STDOUT)
  182. output_utf8 = output.decode("utf-8")
  183. lines = output_utf8.splitlines()
  184. mac=""
  185. for line in lines:
  186. # Station 32:b8:c3:86:3e:e8 (on ibss3)
  187. ml = re.match('^Station ([0-9a-f:]+) \(on ([\w\d]+)\)', line, re.I)
  188. if ml:
  189. mac = ml.group(1)
  190. j[mac] = {}
  191. else:
  192. ml = re.match('^[\t ]+([^:]+):[\t ]+([^ ]+)', line, re.I)
  193. if ml:
  194. j[mac][ml.group(1)] = ml.group(2)
  195. except:
  196. pass
  197. return j
  198. def getNeighbours():
  199. # https://github.com/freifunk-gluon/packages/blob/master/net/respondd/src/respondd.c
  200. j = { "batadv": {}}
  201. stationDump = None
  202. if 'mesh-wlan' in config:
  203. j["wifi"] = {}
  204. stationDump = getStationDump(config["mesh-wlan"])
  205. mesh_ifs = getMesh_Interfaces()
  206. with open("/sys/kernel/debug/batman_adv/" + config['batman'] + "/originators", 'r') as fh:
  207. for line in fh:
  208. #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)
  209. ml = re.match(r"^([0-9a-f:]+)[ ]*([\d\.]*)s[ ]*\(([ ]*\d*)\)[ ]*([0-9a-f:]+)[ ]*\[[ ]*(.*)\]", line, re.I)
  210. if ml:
  211. dev = ml.group(5)
  212. mac_origin = ml.group(1)
  213. mac_nhop = ml.group(4)
  214. tq = ml.group(3)
  215. lastseen = ml.group(2)
  216. if mac_origin == mac_nhop:
  217. if 'mesh-wlan' in config and dev in config["mesh-wlan"] and not stationDump is None:
  218. if not mesh_ifs[dev] in j["wifi"]:
  219. j["wifi"][mesh_ifs[dev]] = {}
  220. j["wifi"][mesh_ifs[dev]]["neighbours"] = {}
  221. if mac_origin in stationDump:
  222. j["wifi"][mesh_ifs[dev]]["neighbours"][mac_origin] = {
  223. "signal": stationDump[mac_origin]["signal"],
  224. "noise": 0, # BUG: fehlt noch
  225. "inactive": stationDump[mac_origin]["inactive time"],
  226. }
  227. if not mesh_ifs[dev] in j["batadv"]:
  228. j["batadv"][mesh_ifs[dev]] = {}
  229. j["batadv"][mesh_ifs[dev]]["neighbours"] = {}
  230. j["batadv"][mesh_ifs[dev]]["neighbours"][mac_origin] = {
  231. "tq": int(tq),
  232. "lastseen": float(lastseen),
  233. }
  234. return j
  235. def getCPUInfo():
  236. j = {}
  237. with open("/proc/cpuinfo", 'r') as fh:
  238. for line in fh:
  239. ml = re.match(r"^(.+?)[\t ]+:[\t ]+(.*)$", line, re.I)
  240. if ml:
  241. j[ml.group(1)] = ml.group(2)
  242. return j
  243. # ======================== Output =========================
  244. # =========================================================
  245. def createNodeinfo():
  246. j = {
  247. "node_id": getNode_ID(),
  248. "hostname": socket.gethostname(),
  249. "network": {
  250. "addresses": getDevice_Addresses(config['bridge']),
  251. "mac": getDevice_MAC(config['batman']),
  252. "mesh": {
  253. "bat0": {
  254. "interfaces": getBat0_Interfaces(),
  255. },
  256. },
  257. "mesh_interfaces": list(getMesh_Interfaces().values()),
  258. },
  259. "software": {
  260. "firmware": {
  261. "base": call(['lsb_release','-ds'])[0],
  262. "release": config['nodeinfo']['software']['firmware']['release'],
  263. },
  264. "batman-adv": {
  265. "version": open('/sys/module/batman_adv/version').read().strip(),
  266. # "compat": # /lib/gluon/mesh-batman-adv-core/compat
  267. },
  268. "status-page": {
  269. "api": 0,
  270. },
  271. "autoupdater": {
  272. # "branch": "stable",
  273. "enabled": False,
  274. },
  275. },
  276. "hardware": {
  277. "model": getCPUInfo()["model name"],
  278. "nproc": int(call(['nproc'])[0]),
  279. },
  280. "system": {
  281. "site_code": config['nodeinfo']['system']['site_code'],
  282. },
  283. "vpn": False,
  284. }
  285. if 'mesh-vpn' in config:
  286. j['fastd'] = {
  287. "version": call(['fastd','-v'])[0].split(' ')[1],
  288. "enabled": True,
  289. },
  290. return merge(j, config['nodeinfo'])
  291. def createStatistics():
  292. j = {
  293. "node_id": getNode_ID(),
  294. "clients": getClients(),
  295. "traffic": getTraffic(),
  296. "idletime": float(open('/proc/uptime').read().split(' ')[1]),
  297. "loadavg": float(open('/proc/loadavg').read().split(' ')[0]),
  298. "memory": getMemory(),
  299. "processes": dict(zip(('running', 'total'), map(int, open('/proc/loadavg').read().split(' ')[3].split('/')))),
  300. "uptime": float(open('/proc/uptime').read().split(' ')[0]),
  301. "mesh_vpn" : { # HopGlass-Server: node.flags.uplink = parsePeerGroup(_.get(n, 'statistics.mesh_vpn'))
  302. "groups": {
  303. "backbone": {
  304. "peers": getMeshVPNPeers(),
  305. },
  306. },
  307. },
  308. }
  309. gateway = getGateway()
  310. if gateway != None:
  311. j["gateway"] = gateway
  312. return j
  313. def createNeighbours():
  314. #/sys/kernel/debug/batman_adv/bat0/originators
  315. j = {
  316. "node_id": getNode_ID(),
  317. }
  318. j = merge(j, getNeighbours())
  319. return j
  320. def sendResponse(request, compress):
  321. json_data = {}
  322. #https://github.com/freifunk-gluon/packages/blob/master/net/respondd/src/respondd.c
  323. if request == 'statistics':
  324. json_data[request] = createStatistics()
  325. elif request == 'nodeinfo':
  326. json_data[request] = createNodeinfo()
  327. elif request == 'neighbours':
  328. json_data[request] = createNeighbours()
  329. else:
  330. print("unknown command: " + request)
  331. return
  332. json_str = bytes(json.dumps(json_data, separators=(',', ':')), 'UTF-8')
  333. if compress:
  334. 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.
  335. gzip_data = encoder.compress(json_str)
  336. gzip_data = gzip_data + encoder.flush()
  337. sock.sendto(gzip_data, sender)
  338. else:
  339. sock.sendto(json_str, sender)
  340. if options["verbose"]:
  341. print(json.dumps(json_data, sort_keys=True, indent=4))
  342. # ===================== Mainfunction ======================
  343. # =========================================================
  344. parser = argparse.ArgumentParser()
  345. parser.add_argument( '-c', '--cfg', default='config.json', metavar='<file>', help='Config File',required=False,)
  346. parser.add_argument( '-d', '--debug', action='store_true', help='Debug Output',required=False,)
  347. parser.add_argument( '-v', '--verbose', action='store_true', help='Verbose Output',required=False)
  348. args = parser.parse_args()
  349. options = vars(args)
  350. config = {}
  351. try:
  352. with open(options['cfg'], 'r') as cfg_handle:
  353. config = json.load(cfg_handle)
  354. except IOError:
  355. raise
  356. if options["debug"]:
  357. print(json.dumps(createNodeinfo(), sort_keys=True, indent=4))
  358. print(json.dumps(createStatistics(), sort_keys=True, indent=4))
  359. print(json.dumps(createNeighbours(), sort_keys=True, indent=4))
  360. #print(json.dumps(getFastd(config["fastd_socket"]), sort_keys=True, indent=4))
  361. #print(json.dumps(getMesh_VPN(), sort_keys=True, indent=4))
  362. sys.exit(1)
  363. if 'addr' in config:
  364. addr = config['addr']
  365. else:
  366. addr = 'ff02::2:1001'
  367. if 'addr' in config:
  368. port = config['port']
  369. else:
  370. port = 1001
  371. if_idx = socket.if_nametoindex(config['bridge'])
  372. group = socket.inet_pton(socket.AF_INET6, addr) + struct.pack("I", if_idx)
  373. sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
  374. sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, group)
  375. sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, bytes(config['bridge'], 'UTF-8'))
  376. sock.bind(('::', port))
  377. # =========================================================
  378. while True:
  379. if select.select([sock],[],[],1)[0]:
  380. msg, sender = sock.recvfrom(2048)
  381. if options["verbose"]:
  382. print(msg)
  383. msg_spl = str(msg, 'UTF-8').split(" ")
  384. if msg_spl[0] == 'GET': # multi_request
  385. for request in msg_spl[1:]:
  386. sendResponse(request, True)
  387. else: # single_request
  388. sendResponse(msg_spl[0], False)