ffpb.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. # -*- coding: utf-8 -*-
  2. from __future__ import print_function
  3. import willie
  4. import datetime
  5. import difflib
  6. from email.utils import mktime_tz
  7. import git
  8. import netaddr
  9. import json
  10. import urllib2
  11. import re
  12. import os
  13. import random
  14. import shelve
  15. import subprocess
  16. import time
  17. import dns.resolver,dns.reversename
  18. import socket
  19. import SocketServer
  20. import threading
  21. msgserver = None
  22. peers_repo = None
  23. monitored_nodes = None
  24. highscores = None
  25. alfred_method = None
  26. alfred_data = None
  27. alfred_update = datetime.datetime(1970,1,1,23,42)
  28. ffpb_resolver = dns.resolver.Resolver ()
  29. ffpb_resolver.nameservers = ['10.132.254.53']
  30. class MsgHandler(SocketServer.BaseRequestHandler):
  31. def handle(self):
  32. data = self.request.recv(2048).strip()
  33. sender = self._resolve_name (self.client_address[0])
  34. bot = self.server.bot
  35. if bot is None:
  36. print("ERROR: No bot in handle() :-(")
  37. return
  38. target = bot.config.core.owner
  39. if bot.config.has_section('ffpb'):
  40. is_public = data.lstrip().lower().startswith("public:")
  41. if is_public and not (bot.config.ffpb.msg_target_public is None):
  42. data = data[7:].lstrip()
  43. target = bot.config.ffpb.msg_target_public
  44. elif not (bot.config.ffpb.msg_target is None):
  45. target = bot.config.ffpb.msg_target
  46. bot.msg(target, "[{0}] {1}".format(sender, str(data)))
  47. def _resolve_name (self, ip):
  48. if ip.startswith ("127."):
  49. return "localhost"
  50. try:
  51. addr = dns.reversename.from_address (ip)
  52. return re.sub ("(.infra)?.ffpb.", "", str (ffpb_resolver.query (addr, "PTR")[0]))
  53. except dns.resolver.NXDOMAIN:
  54. return ip
  55. class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
  56. pass
  57. def setup(bot):
  58. global msgserver, peers_repo, alfred_method, highscores, monitored_nodes
  59. bot.memory['ffpb_in_setup'] = True
  60. highscores = shelve.open('highscoredata', writeback=True)
  61. if not 'nodes' in highscores:
  62. highscores['nodes'] = 0
  63. highscores['nodes_ts'] = time.time()
  64. if not 'clients' in highscores:
  65. highscores['clients'] = 0
  66. highscores['clients_ts'] = time.time()
  67. monitored_nodes = shelve.open('nodes.monitored', writeback=True)
  68. seen_nodes = shelve.open('nodes.seen', writeback=True)
  69. bot.memory['seen_nodes'] = seen_nodes
  70. if not bot.config.has_section('ffpb'):
  71. bot.memory['ffpb_in_setup'] = False
  72. return
  73. if not bot.config.ffpb.peers_directory is None:
  74. peers_repo = git.Repo(bot.config.ffpb.peers_directory)
  75. assert peers_repo.bare == False
  76. if int(bot.config.ffpb.msg_enable) == 1:
  77. host = "localhost"
  78. port = 2342
  79. if not bot.config.ffpb.msg_host is None: host = bot.config.ffpb.msg_host
  80. if not bot.config.ffpb.msg_port is None: port = int(bot.config.ffpb.msg_port)
  81. msgserver = ThreadingTCPServer((host,port), MsgHandler)
  82. msgserver.bot = bot
  83. ip, port = msgserver.server_address
  84. print("Messaging server listening on {}:{}".format(ip,port))
  85. msgserver_thread = threading.Thread(target=msgserver.serve_forever)
  86. msgserver_thread.daemon = True
  87. msgserver_thread.start()
  88. alfred_method = bot.config.ffpb.alfred_method
  89. ffpb_updatealfred(bot)
  90. bot.memory['ffpb_in_setup'] = False
  91. def shutdown(bot):
  92. global msgserver, highscores, monitored_nodes
  93. if not highscores is None:
  94. highscores.sync()
  95. highscores.close()
  96. highscores = None
  97. if not monitored_nodes is None:
  98. monitored_nodes.sync()
  99. monitored_nodes.close()
  100. monitored_nodes = None
  101. if 'seen_nodes' in bot.memory and bot.memory['seen_nodes'] != None:
  102. bot.memory['seen_nodes'].close()
  103. bot.memory['seen_nodes'] = None
  104. del(bot.memory['seen_nodes'])
  105. if not msgserver is None:
  106. msgserver.shutdown()
  107. print("Closed messaging server.")
  108. msgserver = None
  109. @willie.module.commands("help")
  110. @willie.module.commands("hilfe")
  111. @willie.module.commands("man")
  112. def ffpb_help(bot, trigger):
  113. functions = {
  114. "!ping <knoten>": "Prüfe ob der Knoten erreichbar ist.",
  115. "!status": "Aktuellen Status des Netzwerks (insb. Anzahl Knoten und Clients) ausgegeben.",
  116. "!info <knoten>": "Allgemeine Information zu dem Knoten anzeigen.",
  117. "!link <knoten>": "MAC-Adresse und Link zur Status-Seite des Knotens anzeigen.",
  118. "!exec-on-peer <knoten> <kommando>": "Befehl auf dem Knoten ausführen (nur möglich bei eigenen Knoten oder als Admin, in beiden Fällen auch nur wenn der SSH-Key des Bots hinterlegt wurde)",
  119. }
  120. param = trigger.group(2)
  121. if param is None:
  122. bot.say("Funktionen: " + str.join(", ", sorted(functions.keys())))
  123. return
  124. if param.startswith("!"): param = param[1:]
  125. for fun in functions.keys():
  126. if fun.startswith("!" + param + " "):
  127. bot.say("Hilfe zu '" + fun + "': " + functions[fun])
  128. return
  129. bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
  130. def ffpb_findnode(name):
  131. if name is None or len(name) == 0:
  132. return None
  133. name = str(name).strip()
  134. names = {}
  135. # try to match MAC
  136. m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name)
  137. if (not m is None):
  138. mac = m.group(0).lower()
  139. if mac in alfred_data:
  140. return alfred_data[mac]
  141. # try to find alias MAC
  142. for nodeid in alfred_data:
  143. node = alfred_data[nodeid]
  144. if "network" in node:
  145. if "mac" in node["network"] and node["network"]["mac"].lower() == mac:
  146. return node
  147. if "mesh_interfaces" in node["network"]:
  148. for mim in node["network"]["mesh_interfaces"]:
  149. if mim.lower() == mac:
  150. return node
  151. return {
  152. 'hostname': '?-' + mac.replace(':','').lower(),
  153. 'network': { 'addresses': [ mac2ipv6(mac, 'fdca:ffee:ff12:132:') ], 'mac': mac, },
  154. 'hardware': { 'model': 'derived-from-mac' },
  155. }
  156. # look through the ALFRED peers
  157. for nodeid in alfred_data:
  158. node = alfred_data[nodeid]
  159. if 'hostname' in node:
  160. h = node['hostname']
  161. if h.lower() == name.lower():
  162. return node
  163. else:
  164. names[h] = nodeid
  165. # still not found -> try peers_repo
  166. if not peers_repo is None:
  167. peer_name = None
  168. peer_mac = None
  169. peer_file = None
  170. for b in peers_repo.heads.master.commit.tree.blobs:
  171. if b.name.lower() == name.lower():
  172. peer_name = b.name
  173. peer_file = b.abspath
  174. break
  175. if (not peer_file is None) and os.path.exists(peer_file):
  176. peerfile = open(peer_file, "r")
  177. for line in peerfile:
  178. if line.startswith("# MAC:"):
  179. peer_mac = line[6:].strip()
  180. peerfile.close()
  181. if not (peer_mac is None):
  182. return {
  183. 'hostname': peer_name,
  184. 'network': { 'addresses': [ mac2ipv6(peer_mac, 'fdca:ffee:ff12:132:') ], 'mac': peer_mac },
  185. 'hardware': { 'model': 'derived-from-vpnkeys' },
  186. }
  187. # do a similar name lookup in the ALFRED data
  188. possibilities = difflib.get_close_matches(name, [ x for x in names ], cutoff=0.8)
  189. print('findnode: Fuzzy matching \'{0}\' got {1} entries: {2}'.format(name, len(possibilities), ', '.join(possibilities)))
  190. if len(possibilities) == 1:
  191. # if we got exactly one candidate that might be it
  192. return alfred_data[names[possibilities[0]]]
  193. return None
  194. def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata = True):
  195. if (name is None or len(name) == 0):
  196. if not bot is None: bot.reply("Grün.")
  197. return None
  198. if ensure_recent_alfreddata and alfred_data is None:
  199. if not bot is None: bot.say("Informationen sind ausverkauft, kommen erst morgen wieder rein.")
  200. return None
  201. if ensure_recent_alfreddata and ffpb_alfred_data_outdated():
  202. if not bot is None: bot.say("Ich habe gerade keine aktuellen Informationen, daher sage ich mal lieber nichts zu '" + name + "'.")
  203. return None
  204. node = ffpb_findnode(name)
  205. if node is None:
  206. if not bot is None: bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
  207. return node
  208. def mac2ipv6(mac, prefix=None):
  209. result = str(netaddr.EUI(mac).ipv6_link_local())
  210. if (not prefix is None) and (result.startswith("fe80::")):
  211. result = prefix + result[6:]
  212. return result
  213. @willie.module.interval(30)
  214. def ffpb_updatealfred(bot):
  215. """Aktualisiere ALFRED-Daten"""
  216. global alfred_data, alfred_update
  217. if alfred_method is None or alfred_method == "None":
  218. return
  219. updated = None
  220. if alfred_method == "exec":
  221. rawdata = subprocess.check_output(['alfred-json', '-z', '-r', '158'])
  222. updated = datetime.datetime.now()
  223. elif alfred_method.startswith("http"):
  224. try:
  225. rawdata = urllib2.urlopen(alfred_method)
  226. except:
  227. print("Failed to download ALFRED data.")
  228. return
  229. updated = datetime.datetime.fromtimestamp(mktime_tz(rawdata.info().getdate_tz("Last-Modified")))
  230. else:
  231. print("Unknown ALFRED data method '", alfred_method, "', cannot load new data.", sep="")
  232. alfred_data = None
  233. return
  234. try:
  235. alfred_data = json.load(rawdata)
  236. #print("Fetched new ALFRED data:", len(alfred_data), "entries")
  237. alfred_update = updated
  238. except ValueError as e:
  239. print("Failed to parse ALFRED data: " + str(e))
  240. return
  241. seen_nodes = bot.memory['seen_nodes'] if 'seen_nodes' in bot.memory else None
  242. if not seen_nodes is None:
  243. new = []
  244. for nodeid in alfred_data:
  245. nodeid = str(nodeid)
  246. if not nodeid in seen_nodes:
  247. seen_nodes[nodeid] = updated
  248. new.append((nodeid,alfred_data[nodeid]['hostname']))
  249. print('First time seen: ' + str(nodeid))
  250. if len(new) > 0 and not bot.memory['ffpb_in_setup']:
  251. action_msg = None
  252. if len(new) == 1:
  253. action_msg = random.choice((
  254. 'bemerkt den neuen Knoten {0}',
  255. 'entdeckt {0}',
  256. 'reibt sich die Augen und erblickt einen verpackungsfrischen Knoten {0}',
  257. u'heißt {0} im Mesh willkommen',
  258. 'freut sich, dass {0} aufgetaucht ist',
  259. 'traut seinen Augen kaum. {0} sagt zum ersten Mal: Hallo Freifunk Paderborn',
  260. u'sieht die ersten Herzschläge von {0}',
  261. u'stellt einen großen Pott Heißgetränk zu {0} und fragt ob es hier Meshpartner gibt.',
  262. )).format('\'' + str(new[0][1]) + '\'')
  263. else:
  264. action_msg = random.choice((
  265. 'bemerkt die neuen Knoten {0} und {1}',
  266. 'hat {0} und {1} entdeckt',
  267. 'bewundert {0} sowie {1}',
  268. 'freut sich, dass {0} und {1} nun auch online sind',
  269. u'heißt {0} und {1} im Mesh willkommen',
  270. 'fragt sich ob die noch jungen Herzen von {0} und {1} synchron schlagen',
  271. )).format('\'' + '\', \''.join([ str(x[1]) for x in new[0:-1] ]) + '\'', '\'' + str(new[-1][1]) + '\'')
  272. action_target = bot.config.ffpb.msg_target
  273. bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
  274. def ffpb_alfred_data_outdated():
  275. timeout = datetime.datetime.now() - datetime.timedelta(minutes=5)
  276. is_outdated = timeout > alfred_update
  277. #print("ALFRED outdated? {0} (timeout={1} vs. lastupdate={2})".format(is_outdated, timeout, alfred_update))
  278. return is_outdated
  279. @willie.module.commands('debug-alfred')
  280. def ffpb_debug_alfred(bot, trigger):
  281. if alfred_data is None:
  282. bot.say("Keine ALFRED-Daten vorhanden.")
  283. else:
  284. bot.say("ALFRED Daten: count={0} lastupdate={1}".format(len(alfred_data), alfred_update))
  285. @willie.module.commands('alfred-data')
  286. def ffpb_peerdata(bot, trigger):
  287. if (not trigger.admin):
  288. bot.say('I wont leak (possibly) sensitive data to you.')
  289. return
  290. if (not trigger.is_privmsg) and (not trigger.nick in bot.ops[trigger.sender]):
  291. bot.say('Kein Keks? Keine Daten.')
  292. return
  293. target_name = trigger.group(2)
  294. node = ffpb_findnode_from_botparam(bot, target_name)
  295. if node is None: return
  296. for key in node:
  297. if key in [ 'hostname' ]: continue
  298. bot.say("{0}.{1} = {2}".format(node['hostname'], key, str(node[key])))
  299. @willie.module.commands('info')
  300. def ffpb_peerinfo(bot, trigger):
  301. target_name = trigger.group(2)
  302. node = ffpb_findnode_from_botparam(bot, target_name)
  303. if node is None: return
  304. info_mac = node["network"]["mac"]
  305. info_name = node["hostname"]
  306. info_hw = ""
  307. if "hardware" in node:
  308. if "model" in node["hardware"]:
  309. model = node["hardware"]["model"]
  310. info_hw = " model='" + model + "'"
  311. info_fw = ""
  312. info_update = ""
  313. if "software" in node:
  314. if "firmware" in node["software"]:
  315. fwinfo = str(node["software"]["firmware"]["release"]) if "release" in node["software"]["firmware"] else "unknown"
  316. info_fw = " firmware=" + fwinfo
  317. if "autoupdater" in node["software"]:
  318. autoupdater = node["software"]["autoupdater"]["branch"] if node["software"]["autoupdater"]["enabled"] else "off"
  319. info_update = " (autoupdater="+autoupdater+")"
  320. info_uptime = ""
  321. u = -1
  322. if "statistics" in node and "uptime" in node["statistics"]:
  323. u = int(float(node["statistics"]["uptime"]))
  324. elif 'uptime' in node:
  325. u = int(float(node['uptime']))
  326. if u > 0:
  327. d, r1 = divmod(u, 86400)
  328. h, r2 = divmod(r1, 3600)
  329. m, s = divmod(r2, 60)
  330. if d > 0:
  331. info_uptime = ' up {0}d {1}h'.format(d,h)
  332. elif h > 0:
  333. info_uptime = ' up {0}h {1}m'.format(h,m)
  334. else:
  335. info_uptime = ' up {0}m'.format(m)
  336. bot.say('[{1}]{2}{3}{4}{5}'.format(info_mac, info_name, info_hw, info_fw, info_update, info_uptime))
  337. @willie.module.commands('uptime')
  338. def ffpb_peeruptime(bot, trigger):
  339. target_name = trigger.group(2)
  340. node = ffpb_findnode_from_botparam(bot, target_name)
  341. if node is None: return
  342. info_name = node["hostname"]
  343. info_uptime = ''
  344. u_raw = None
  345. if 'statistics' in node and 'uptime' in node['statistics']:
  346. u_raw = node['statistics']['uptime']
  347. elif 'uptime' in node:
  348. u_raw = node['uptime']
  349. if not u_raw is None:
  350. u = int(float(u_raw))
  351. d, r1 = divmod(u, 86400)
  352. h, r2 = divmod(r1, 3600)
  353. m, s = divmod(r2, 60)
  354. if d > 0:
  355. info_uptime += '{0}d '.format(d)
  356. if h > 0:
  357. info_uptime += '{0}h '.format(h)
  358. info_uptime += '{0}m'.format(m)
  359. info_uptime += ' # raw: \'{0}\''.format(u_raw)
  360. else:
  361. info_uptime += '?'
  362. bot.say('uptime(\'{0}\') = {1}'.format(info_name, info_uptime))
  363. @willie.module.commands('link')
  364. def ffpb_peerlink(bot, trigger):
  365. target_name = trigger.group(2)
  366. node = ffpb_findnode_from_botparam(bot, target_name)
  367. if node is None: return
  368. info_mac = node["network"]["mac"]
  369. info_name = node["hostname"]
  370. info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:')
  371. bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6))
  372. @willie.module.interval(60)
  373. def ffpb_updatepeers(bot):
  374. """Aktualisiere die Knotenliste und melde das Diff"""
  375. if peers_repo is None:
  376. print('WARNING: peers_repo is None')
  377. return
  378. old_head = peers_repo.head.commit
  379. peers_repo.remotes.origin.pull()
  380. new_head = peers_repo.head.commit
  381. if new_head != old_head:
  382. print('git pull: from ' + str(old_head) + ' to ' + str(new_head))
  383. added = []
  384. changed = []
  385. renamed = []
  386. deleted = []
  387. for f in old_head.diff(new_head):
  388. if f.new_file:
  389. added.append(f.b_blob.name)
  390. elif f.deleted_file:
  391. deleted.append(f.a_blob.name)
  392. elif f.renamed:
  393. renamed.append([f.rename_from, f.rename_to])
  394. else:
  395. changed.append(f.a_blob.name)
  396. response = "Knoten-Update (VPN +{0} %{1} -{2}): ".format(len(added), len(renamed)+len(changed), len(deleted))
  397. for f in added:
  398. response += " +'{}'".format(f)
  399. for f in changed:
  400. response += " %'{}'".format(f)
  401. for f in renamed:
  402. response += " '{}'->'{}'".format(f[0],f[1])
  403. for f in deleted:
  404. response += " -'{}'".format(f)
  405. bot.msg(bot.config.ffpb.msg_target, response)
  406. def ffpb_fetch_stats(bot, url, memoryid):
  407. response = urllib2.urlopen(url)
  408. data = json.load(response)
  409. nodes_active = 0
  410. nodes_total = 0
  411. clients_count = 0
  412. for node in data['nodes']:
  413. if node['flags']['gateway'] or node['flags']['client']:
  414. continue
  415. nodes_total += 1
  416. if node['flags']['online']:
  417. nodes_active += 1
  418. if 'legacy' in node['flags'] and node['flags']['legacy']:
  419. clients_count -= 1
  420. for link in data['links']:
  421. if link['type'] == 'client':
  422. clients_count += 1
  423. if not memoryid in bot.memory:
  424. bot.memory[memoryid] = { }
  425. stats = bot.memory[memoryid]
  426. stats["fetchtime"] = time.time()
  427. stats["nodes_active"] = nodes_active
  428. stats["nodes_total"] = nodes_total
  429. stats["clients"] = clients_count
  430. return (nodes_active, nodes_total, clients_count)
  431. @willie.module.interval(15)
  432. def ffpb_get_stats(bot):
  433. (nodes_active, nodes_total, clients_count) = ffpb_fetch_stats(bot, 'http://map.paderborn.freifunk.net/nodes.json', 'ffpb_stats')
  434. highscore_changed = False
  435. if nodes_active > highscores['nodes']:
  436. highscores['nodes'] = nodes_active
  437. highscores['nodes_ts'] = time.time()
  438. highscore_changed = True
  439. if clients_count > highscores['clients']:
  440. highscores['clients'] = clients_count
  441. highscores['clients_ts'] = time.time()
  442. highscore_changed = True
  443. if highscore_changed:
  444. print('HIGHSCORE changed: {0} nodes ({1}), {2} clients ({3})'.format(highscores['nodes'], highscores['nodes_ts'], highscores['clients'], highscores['clients_ts']))
  445. if not (bot.config.ffpb.msg_target is None):
  446. action_msg = 'notiert sich den neuen Highscore: {0} Knoten ({1}), {2} Clients ({3})'.format(highscores['nodes'], pretty_date(int(highscores['nodes_ts'])), highscores['clients'], pretty_date(int(highscores['clients_ts'])))
  447. action_target = bot.config.ffpb.msg_target
  448. if (not bot.config.ffpb.msg_target_public is None):
  449. action_target = bot.config.ffpb.msg_target_public
  450. bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
  451. @willie.module.commands('status')
  452. def ffpb_status(bot, trigger):
  453. """Status des FFPB-Netzes: Anzahl (aktiver) Knoten + Clients"""
  454. stats = bot.memory['ffpb_stats'] if 'ffpb_stats' in bot.memory else None
  455. if stats is None:
  456. bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?')
  457. return
  458. bot.say('Es sind {0} Knoten und ca. {1} Clients online.'.format(stats["nodes_active"], stats["clients"]))
  459. def pretty_date(time=False):
  460. """
  461. Get a datetime object or a int() Epoch timestamp and return a
  462. pretty string like 'an hour ago', 'Yesterday', '3 months ago',
  463. 'just now', etc
  464. """
  465. from datetime import datetime
  466. now = datetime.now()
  467. compare = None
  468. if type(time) is int:
  469. compare = datetime.fromtimestamp(time)
  470. elif type(time) is float:
  471. compare = datetime.fromtimestamp(int(time))
  472. elif isinstance(time,datetime):
  473. compare = time
  474. elif not time:
  475. compare = now
  476. diff = now - compare
  477. second_diff = diff.seconds
  478. day_diff = diff.days
  479. if day_diff < 0:
  480. return ''
  481. if day_diff == 0:
  482. if second_diff < 10:
  483. return "gerade eben"
  484. if second_diff < 60:
  485. return "vor " + str(second_diff) + " Sekunden"
  486. if second_diff < 120:
  487. return "vor einer Minute"
  488. if second_diff < 3600:
  489. return "vor " + str(second_diff / 60) + " Minuten"
  490. if second_diff < 7200:
  491. return "vor einer Stunde"
  492. if second_diff < 86400:
  493. return "vor " + str(second_diff / 3600) + " Stunden"
  494. if day_diff == 1:
  495. return "gestern"
  496. if day_diff < 7:
  497. return "vor " + str(day_diff) + " Tagen"
  498. return "am " + compare.strftime('%d.%m.%Y um %H:%M Uhr')
  499. @willie.module.commands('highscore')
  500. def ffpb_highscore(bot, trigger):
  501. bot.say('Highscore: {0} Knoten ({1}), {2} Clients ({3})'.format(
  502. highscores['nodes'], pretty_date(int(highscores['nodes_ts'])),
  503. highscores['clients'], pretty_date(int(highscores['clients_ts']))))
  504. @willie.module.commands('rollout-status')
  505. def ffpb_rolloutstatus(bot, trigger):
  506. result = { }
  507. for branch in [ 'stable', 'testing' ]:
  508. result[branch] = None
  509. skipped = 0
  510. if (not (trigger.admin and trigger.is_privmsg)) and (not trigger.nick in bot.ops[trigger.sender]):
  511. bot.say('Geh zur dunklen Seite, die haben Kekse - ohne Keks kein Rollout-Status.')
  512. return
  513. expected_release = trigger.group(2)
  514. if expected_release is None or len(expected_release) == 0:
  515. bot.say('Von welcher Firmware denn?')
  516. return
  517. for nodeid in alfred_data:
  518. item = alfred_data[nodeid]
  519. if (not 'software' in item) or (not 'firmware' in item['software']) or (not 'autoupdater' in item['software']):
  520. skipped+=1
  521. continue
  522. release = item['software']['firmware']['release']
  523. branch = item['software']['autoupdater']['branch']
  524. enabled = item['software']['autoupdater']['enabled']
  525. if not branch in result or result[branch] is None:
  526. result[branch] = { 'auto_count': 0, 'auto_not': 0, 'manual_count': 0, 'manual_not': 0, 'total': 0 }
  527. result[branch]['total'] += 1
  528. match = 'count' if release == expected_release else 'not'
  529. mode = 'auto' if enabled else 'manual'
  530. result[branch][mode+'_'+match] += 1
  531. output = "Rollout von '{0}':".format(expected_release)
  532. for branch in result:
  533. auto_count = result[branch]['auto_count']
  534. auto_total = auto_count + result[branch]['auto_not']
  535. manual_count = result[branch]['manual_count']
  536. manual_total = manual_count + result[branch]['manual_not']
  537. bot.say("Rollout von '{0}': {1} = {2}/{3} per Auto-Update, {4}/{5} manuell".format(expected_release, branch, auto_count, auto_total, manual_count, manual_total))
  538. if skipped > 0:
  539. bot.say("Rollout von '{0}': {1} Knoten unklar".format(expected_release, skipped))
  540. @willie.module.commands('ping')
  541. def ffpb_ping(bot, trigger=None, target_name=None):
  542. """Ping FFPB-Knoten"""
  543. if target_name is None: target_name = trigger.group(2)
  544. node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
  545. if node is None: return None
  546. target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
  547. target_alias = node["hostname"]
  548. print("pinging '{0}' at {1} ...".format(target_name, target))
  549. result = os.system('ping6 -c 2 -W 1 ' + target + ' >/dev/null')
  550. if result == 0:
  551. print("ping to '{0}' succeeded".format(target_name))
  552. if not bot is None: bot.say('Knoten "' + target_alias + '" antwortet \o/')
  553. return True
  554. elif result == 1 or result == 256:
  555. print("ping to '{0}' failed".format(target_name))
  556. if not bot is None: bot.say('Keine Antwort von "' + target_alias + '" :-(')
  557. return False
  558. else:
  559. print("ping to '{0}' broken: result='{1}'".format(target_name, result))
  560. if not bot is None: bot.say('Uh oh, irgendwas ist kaputt. Chef, ping result = ' + str(result) + ' - darf ich das essen?')
  561. return None
  562. @willie.module.interval(3*60)
  563. def ffpb_monitor_ping(bot):
  564. notify_target = bot.config.core.owner
  565. if (not bot.config.ffpb.msg_target is None):
  566. notify_target = bot.config.ffpb.msg_target
  567. for node in monitored_nodes:
  568. mon = monitored_nodes[node]
  569. added = mon['added']
  570. last_status = mon['status']
  571. last_check = mon['last_check']
  572. last_success = mon['last_success']
  573. current_status = ffpb_ping(bot=None, target_name=node)
  574. if current_status is None: current_status = False
  575. mon['status'] = current_status
  576. mon['last_check'] = time.time()
  577. if current_status == True: mon['last_success'] = time.time()
  578. print("Monitoring ({0}) {1} (last: {2} at {3})".format(node, current_status, last_status, time.strftime('%Y-%m-%d %H:%M', time.localtime(last_check))))
  579. if last_status != current_status and (last_status or current_status):
  580. if last_check is None:
  581. # erster Check, keine Ausgabe
  582. continue
  583. if current_status == True:
  584. bot.msg(notify_target, 'Monitoring: Knoten \'{0}\' pingt wieder (zuletzt {1})'.format(node, pretty_date(last_success) if not last_success is None else '[nie]'))
  585. else:
  586. bot.msg(notify_target, 'Monitoring: Knoten \'{0}\' DOWN'.format(node))
  587. @willie.module.commands('monitor')
  588. def ffpb_monitor(bot, trigger):
  589. if not trigger.admin:
  590. bot.say('Ich ping hier nicht für jeden durch die Weltgeschichte.')
  591. return
  592. if trigger.group(2) is None or len(trigger.group(2)) == 0:
  593. bot.say('Das Monitoring sagt du hast doofe Ohren.')
  594. return
  595. cmd = trigger.group(3)
  596. node = trigger.group(4)
  597. if not node is None: node = str(node)
  598. if cmd == "add":
  599. if node in monitored_nodes:
  600. bot.say('Knoten \'{0}\' wird bereits gemonitored.'.format(node))
  601. return
  602. monitored_nodes[node] = {
  603. 'added': trigger.sender,
  604. 'status': None,
  605. 'last_check': None,
  606. 'last_success': None,
  607. }
  608. bot.say('Knoten \'{0}\' wird jetzt ganz genau beobachtet.'.format(node))
  609. return
  610. if cmd == "del":
  611. if not node in monitored_nodes:
  612. bot.say('Knoten \'{0}\' war gar nicht im Monitoring?!?'.format(node))
  613. return
  614. del monitored_nodes[node]
  615. bot.say('Okidoki, \'{0}\' lasse ich jetzt links liegen.'.format(node))
  616. return
  617. if cmd == "info":
  618. if node in monitored_nodes:
  619. info = monitored_nodes[node]
  620. bot.say('Knoten \'{0}\' wurde zuletzt {1} gepingt (Ergebnis: {2})'.format(node, pretty_date(info['last_check']) if not info['last_check'] is None else "^W noch nie", info['status']))
  621. else:
  622. bot.say('Knoten \'{0}\' ist nicht im Monitoring.'.format(node))
  623. return
  624. if cmd == "list":
  625. nodes = ""
  626. for node in monitored_nodes:
  627. nodes = nodes + " " + node
  628. bot.say('Monitoring aktiv für:' + nodes)
  629. return
  630. if cmd == "help":
  631. bot.say('Entweder "!monitor list" oder "!monitor {add|del|info} <node>"')
  632. return
  633. bot.say('Mit "' + str(cmd) + '" kann ich nix anfangen, probier doch mal "!monitor help".')
  634. @willie.module.commands('providers')
  635. def ffpb_providers(bot, trigger):
  636. providers = json.load(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/providers?format=json'))
  637. providers.sort(key=lambda x: x['count'], reverse=True)
  638. bot.say('Unsere Top 5 Provider: ' + ', '.join(['{0} ({1:.0f}%)'.format(x['name'], x['percentage']) for x in providers[:5]]))
  639. @willie.module.commands('neighbours')
  640. def ffpb_neighbours(bot, trigger):
  641. target_name = trigger.group(2)
  642. node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
  643. if node is None: return None
  644. nodeid = node['node_id'] if 'node_id' in node else node['mac'].replace(':','')
  645. cave_result = json.load(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/node/{0}/neighbours'.format(nodeid)))
  646. d = '&'.join([ str(n) for n in cave_result ])
  647. req = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/idmac2name', d)
  648. neighbours = []
  649. for n in req:
  650. ident,name = n.strip().split('=')
  651. neighbours.append(name)
  652. bot.say('; '.join(neighbours))
  653. @willie.module.commands('exec-on-peer')
  654. def ffpb_remoteexec(bot, trigger):
  655. """Remote Execution fuer FFPB_Knoten"""
  656. bot_params = trigger.group(2).split(' ',1)
  657. if len(bot_params) != 2:
  658. bot.say('Wenn du nicht sagst wo mach ich remote execution bei dir!')
  659. bot.say('Tipp: !exec-on-peer <peer> <cmd>')
  660. return
  661. target_name = bot_params[0]
  662. target_cmd = bot_params[1]
  663. if not trigger.admin:
  664. bot.say('I can haz sudo?')
  665. return
  666. if trigger.is_privmsg:
  667. bot.say('Bitte per Channel.')
  668. return
  669. if not trigger.nick in bot.ops[trigger.sender]:
  670. bot.say('Geh weg.')
  671. return
  672. node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
  673. if node is None: return
  674. target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
  675. target_alias = node["hostname"]
  676. cmd = 'ssh -6 -l root ' + target + ' -- "' + target_cmd + '"'
  677. print("REMOTE EXEC = " + cmd)
  678. try:
  679. result = subprocess.check_output(['ssh', '-6n', '-l', 'root', '-o', 'BatchMode=yes', '-o','StrictHostKeyChecking=no', target, target_cmd], stderr=subprocess.STDOUT, shell=False)
  680. lines = str(result).splitlines()
  681. if len(lines) == 0:
  682. bot.say('exec-on-peer(' + target_alias + '): No output')
  683. return
  684. msg = 'exec-on-peer(' + target_alias + '): ' + str(len(lines)) + ' Zeilen'
  685. if len(lines) > 8:
  686. msg += ' (zeige max. 8)'
  687. bot.say(msg + ':')
  688. for line in lines[0:8]:
  689. bot.say(line)
  690. except subprocess.CalledProcessError as e:
  691. bot.say('Fehler '+str(e.returncode)+' bei exec-on-peer('+target_alias+'): ' + e.output)