ffpb.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. # -*- coding: utf-8 -*-
  2. from __future__ import print_function
  3. import willie
  4. import git
  5. import netaddr
  6. import json
  7. import urllib2
  8. import re
  9. import os
  10. import subprocess
  11. import dns.resolver,dns.reversename
  12. import socket
  13. import SocketServer
  14. import threading
  15. msgserver = None
  16. peers_repo = None
  17. stats = None
  18. alfred_method = None
  19. alfred_data = None
  20. ffpb_resolver = dns.resolver.Resolver ()
  21. ffpb_resolver.nameservers = ['10.132.254.53']
  22. class MsgHandler(SocketServer.BaseRequestHandler):
  23. def handle(self):
  24. data = self.request.recv(2048).strip()
  25. sender = self._resolve_name (self.client_address[0])
  26. bot = self.server.bot
  27. if bot is None:
  28. print("ERROR: No bot in handle() :-(")
  29. return
  30. target = bot.config.core.owner
  31. if bot.config.has_section('ffpb'):
  32. is_public = data.lstrip().lower().startswith("public:")
  33. if is_public and not (bot.config.ffpb.msg_target_public is None):
  34. data = data[7:].lstrip()
  35. target = bot.config.ffpb.msg_target_public
  36. elif not (bot.config.ffpb.msg_target is None):
  37. target = bot.config.ffpb.msg_target
  38. bot.msg(target, "[{0}] {1}".format(sender, str(data)))
  39. def _resolve_name (self, ip):
  40. if ip.startswith ("127."):
  41. return "localhost"
  42. try:
  43. addr = dns.reversename.from_address (ip)
  44. return re.sub ("(.infra)?.ffpb.", "", str (ffpb_resolver.query (addr, "PTR")[0]))
  45. except dns.resolver.NXDOMAIN:
  46. return ip
  47. class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
  48. pass
  49. def setup(bot):
  50. global msgserver, peers_repo, alfred_method
  51. if not bot.config.has_section('ffpb'):
  52. return
  53. if not bot.config.ffpb.peers_directory is None:
  54. peers_repo = git.Repo(bot.config.ffpb.peers_directory)
  55. assert peers_repo.bare == False
  56. if int(bot.config.ffpb.msg_enable) == 1:
  57. host = "localhost"
  58. port = 2342
  59. if not bot.config.ffpb.msg_host is None: host = bot.config.ffpb.msg_host
  60. if not bot.config.ffpb.msg_port is None: port = int(bot.config.ffpb.msg_port)
  61. msgserver = ThreadingTCPServer((host,port), MsgHandler)
  62. msgserver.bot = bot
  63. ip, port = msgserver.server_address
  64. print("Messaging server listening on {}:{}".format(ip,port))
  65. msgserver_thread = threading.Thread(target=msgserver.serve_forever)
  66. msgserver_thread.daemon = True
  67. msgserver_thread.start()
  68. alfred_method = bot.config.ffpb.alfred_method
  69. ffpb_updatealfred(bot)
  70. def shutdown(bot):
  71. global msgserver
  72. if not msgserver is None:
  73. msgserver.shutdown()
  74. print("Closed messaging server.")
  75. msgserver = None
  76. def ffpb_findnode(name):
  77. if name is None or len(name) == 0:
  78. return None
  79. name = str(name).strip()
  80. # try to match MAC
  81. m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name)
  82. if (not m is None):
  83. mac = m.group(0).lower()
  84. if mac in alfred_data:
  85. return alfred_data[mac]
  86. # try to find alias MAC
  87. for nodeid in alfred_data:
  88. node = alfred_data[nodeid]
  89. if "network" in node:
  90. if "mac" in node["network"] and node["network"]["mac"].lower() == mac:
  91. return node
  92. if "mesh_interfaces" in node["network"]:
  93. for mim in node["network"]["mesh_interfaces"]:
  94. if mim.lower() == mac:
  95. return node
  96. # look through the ALFRED peers
  97. possible_matches = []
  98. for nodeid in alfred_data:
  99. node = alfred_data[nodeid]
  100. if "hostname" in node and node["hostname"].lower() == name.lower():
  101. return node
  102. # still not found -> try peers_repo
  103. if not peers_repo is None:
  104. peer_name = None
  105. peer_mac = None
  106. peer_file = None
  107. for b in peers_repo.heads.master.commit.tree.blobs:
  108. if b.name.lower() == name.lower():
  109. peer_name = b.name
  110. peer_file = b.abspath
  111. break
  112. if (not peer_file is None) and os.path.exists(peer_file):
  113. peerfile = open(peer_file, "r")
  114. for line in peerfile:
  115. if line.startswith("# MAC:"):
  116. peer_mac = line[6:].strip()
  117. peerfile.close()
  118. if not (peer_mac is None):
  119. return { "hostname": peer_name, "network": { "addresses": [ mac2ipv6(peer_mac, "fdca:ffee:ff12:132:") ], "mac": peer_mac } }
  120. return None
  121. def mac2ipv6(mac, prefix=None):
  122. result = str(netaddr.EUI(mac).ipv6_link_local())
  123. if (not prefix is None) and (result.startswith("fe80::")):
  124. result = prefix + result[6:]
  125. return result
  126. @willie.module.interval(30)
  127. def ffpb_updatealfred(bot):
  128. """Aktualisiere ALFRED-Daten"""
  129. global alfred_data
  130. if alfred_method is None or alfred_method == "None":
  131. return
  132. if alfred_method == "exec":
  133. rawdata = subprocess.check_output(['alfred-json', '-z', '-r', '158'])
  134. alfred_data = json.load(rawdata)
  135. #print("Fetched new ALFRED data:", len(alfred_data), "entries")
  136. return
  137. if alfred_method.startswith("http"):
  138. rawdata = urllib2.urlopen(alfred_method)
  139. alfred_data = json.load(rawdata)
  140. #print("Downloaded new ALFRED data:", len(alfred_data), "entries")
  141. return
  142. print("Unknown ALFRED data method '", alfred_method, "', cannot load new data.", sep="")
  143. alfred_data = None
  144. @willie.module.commands('info')
  145. def ffpb_peerinfo(bot, trigger):
  146. target_name = trigger.group(2)
  147. if (target_name is None or len(target_name) == 0):
  148. bot.say(str(trigger.nick + ": Grün."))
  149. return
  150. if alfred_data is None:
  151. bot.say("Informationen sind ausverkauft, kommen erst morgen wieder rein.")
  152. return
  153. node = ffpb_findnode(target_name)
  154. if node is None:
  155. bot.say("Kein Plan wer oder was mit '" + target_name + "' gemeint ist :(")
  156. return
  157. info_mac = node["network"]["mac"]
  158. info_name = node["hostname"]
  159. info_hw = ""
  160. if "hardware" in node:
  161. if "model" in node["hardware"]:
  162. model = node["hardware"]["model"]
  163. info_hw = " model='" + model + "'"
  164. info_fw = ""
  165. info_update = ""
  166. if "software" in node:
  167. if "firmware" in node["software"]:
  168. fwinfo = str(node["software"]["firmware"]["release"]) if "release" in node["software"]["firmware"] else "unknown"
  169. info_fw = " firmware=" + fwinfo
  170. if "autoupdater" in node["software"]:
  171. autoupdater = node["software"]["autoupdater"]["branch"] if node["software"]["autoupdater"]["enabled"] else "off"
  172. info_update = " (autoupdater="+autoupdater+")"
  173. info_uptime = ""
  174. if "statistics" in node and "uptime" in node["statistics"]:
  175. u = int(float(node["statistics"]["uptime"]))
  176. d, r1 = divmod(int(float(node["statistics"]["uptime"])), 86400)
  177. h, r2 = divmod(r1, 3600)
  178. m, s = divmod(r2, 60)
  179. if d > 0:
  180. info_uptime = ' up {0}d {1}h'.format(d,h)
  181. elif h > 0:
  182. info_uptime = ' up {0}h {1}m'.format(h,m)
  183. else:
  184. info_uptime = ' up {0}m'.format(m)
  185. bot.say('[{1}]{2}{3}{4}{5}'.format(info_mac, info_name, info_hw, info_fw, info_update, info_uptime))
  186. @willie.module.commands('link')
  187. def ffpb_peerlink(bot, trigger):
  188. target_name = trigger.group(2)
  189. if (target_name is None or len(target_name) == 0):
  190. bot.say(str(trigger.nick + ": Grün."))
  191. return
  192. if alfred_data is None:
  193. bot.say("Informationen sind ausverkauft, kommen erst morgen wieder rein.")
  194. return
  195. node = ffpb_findnode(target_name)
  196. if node is None:
  197. bot.say("Kein Plan wer oder was mit '" + target_name + "' gemeint ist :(")
  198. return
  199. info_mac = node["network"]["mac"]
  200. info_name = node["hostname"]
  201. info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:')
  202. bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6))
  203. @willie.module.interval(60)
  204. def ffpb_updatepeers(bot):
  205. """Aktualisiere die Knotenliste und melde das Diff"""
  206. if peers_repo is None:
  207. print('WARNING: peers_repo is None')
  208. return
  209. old_head = peers_repo.head.commit
  210. peers_repo.remotes.origin.pull()
  211. new_head = peers_repo.head.commit
  212. if new_head != old_head:
  213. print('git pull: from ' + str(old_head) + ' to ' + str(new_head))
  214. added = []
  215. changed = []
  216. renamed = []
  217. deleted = []
  218. for f in old_head.diff(new_head):
  219. if f.new_file:
  220. added.append(f.b_blob.name)
  221. elif f.deleted_file:
  222. deleted.append(f.a_blob.name)
  223. elif f.renamed:
  224. renamed.append([f.rename_from, f.rename_to])
  225. else:
  226. changed.append(f.a_blob.name)
  227. response = "Knoten-Update (VPN +{0} %{1} -{2}): ".format(len(added), len(renamed)+len(changed), len(deleted))
  228. for f in added:
  229. response += " +'{}'".format(f)
  230. for f in changed:
  231. response += " %'{}'".format(f)
  232. for f in renamed:
  233. response += " '{}'->'{}'".format(f[0],f[1])
  234. for f in deleted:
  235. response += " -'{}'".format(f)
  236. bot.msg(bot.config.ffpb.msg_target, response)
  237. @willie.module.interval(15)
  238. def ffpb_get_stats(bot):
  239. global stats
  240. response = urllib2.urlopen('http://map.paderborn.freifunk.net/nodes.json')
  241. data = json.load(response)
  242. nodes_active = 0
  243. nodes_total = 0
  244. clients_count = 0
  245. for node in data['nodes']:
  246. if node['flags']['gateway'] or node['flags']['client']:
  247. continue
  248. nodes_total += 1
  249. if node['flags']['online']:
  250. nodes_active += 1
  251. for link in data['links']:
  252. if link['type'] == 'client':
  253. clients_count += 1
  254. if stats is None:
  255. stats = { }
  256. stats["nodes_active"] = nodes_active
  257. stats["nodes_total"] = nodes_total
  258. stats["clients"] = clients_count
  259. @willie.module.commands('status')
  260. def ffpb_status(bot, trigger):
  261. """Status des FFPB-Netzes: Anzahl (aktiver) Knoten + Clients"""
  262. if stats is None:
  263. bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?')
  264. return
  265. bot.say('Es sind {0} Knoten und ca. {1} Clients online.'.format(stats["nodes_active"], stats["clients"]))
  266. @willie.module.commands('ping')
  267. def ffpb_ping(bot, trigger):
  268. """Ping FFPB-Knoten"""
  269. target_name = trigger.group(2)
  270. if target_name is None or len(target_name) == 0:
  271. bot.say('Alter, wen soll ich denn pingen? Einmal mit Profis arbeiten -.-')
  272. return
  273. node = ffpb_findnode(target_name)
  274. if node is None:
  275. bot.say('Kein Plan wer mit \'' + target_name + '\' gemeint ist :/')
  276. return
  277. target = node["network"]["addresses"][0]
  278. target_alias = node["hostname"]
  279. print("ping '", target , '"', sep='')
  280. result = os.system('ping6 -c 2 -W 1 ' + target + ' 2>/dev/null')
  281. if result == 0:
  282. bot.say('Knoten "' + target_alias + '" antwortet \o/')
  283. elif result == 1 or result == 256:
  284. bot.say('Keine Antwort von "' + target_alias + '" :-(')
  285. else:
  286. bot.say('Uh oh, irgendwas ist kaputt. Chef, ping result = ' + str(result) + ' - darf ich das essen?')
  287. @willie.module.commands('exec-on-peer')
  288. def ffpb_remoteexec(bot, trigger):
  289. """Remote Execution fuer FFPB_Knoten"""
  290. bot_params = trigger.group(2).split(' ',1)
  291. if len(bot_params) != 2:
  292. bot.say('Wenn du nicht sagst wo mach ich remote execution bei dir!')
  293. bot.say('Tipp: !exec-on-peer <peer> <cmd>')
  294. return
  295. target_name = bot_params[0]
  296. target_cmd = bot_params[1]
  297. if not trigger.admin:
  298. bot.say('I can haz sudo?')
  299. return
  300. if trigger.is_privmsg:
  301. bot.say('Bitte per Channel.')
  302. return
  303. if not trigger.nick in bot.ops[trigger.sender]:
  304. bot.say('Geh weg.')
  305. return
  306. node = ffpb_findnode(target_name)
  307. if node is None:
  308. bot.say('Kein Plan wer mit \'' + target_name + '\' gemeint ist :/')
  309. return
  310. target = node["network"]["addresses"][0]
  311. target_alias = node["hostname"]
  312. cmd = 'ssh -6 -l root ' + target + ' -- "' + target_cmd + '"'
  313. print("REMOTE EXEC = " + cmd)
  314. try:
  315. result = subprocess.check_output(['ssh', '-6n', '-l', 'root', '-o', 'BatchMode=yes', '-o','StrictHostKeyChecking=no', target, target_cmd], stderr=subprocess.STDOUT, shell=False)
  316. lines = str(result).splitlines()
  317. if len(lines) == 0:
  318. bot.say('exec-on-peer(' + target_alias + '): No output')
  319. return
  320. msg = 'exec-on-peer(' + target_alias + '): ' + str(len(lines)) + ' Zeilen'
  321. if len(lines) > 8:
  322. msg += ' (zeige max. 8)'
  323. bot.say(msg + ':')
  324. for line in lines[0:8]:
  325. bot.say(line)
  326. except subprocess.CalledProcessError as e:
  327. bot.say('Fehler '+str(e.returncode)+' bei exec-on-peer('+target_alias+'): ' + e.output)