ffpb.py 24 KB

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