ffpb.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  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. alfred_method = None
  23. ffpb_resolver = dns.resolver.Resolver ()
  24. ffpb_resolver.nameservers = ['10.132.254.53']
  25. class MsgHandler(SocketServer.BaseRequestHandler):
  26. """Reads line from TCP stream and forwards it to configured IRC channels."""
  27. def handle(self):
  28. data = self.request.recv(2048).strip()
  29. sender = self._resolve_name (self.client_address[0])
  30. bot = self.server.bot
  31. if bot is None:
  32. print("ERROR: No bot in handle( ) :-(")
  33. return
  34. target = bot.config.core.owner
  35. if bot.config.has_section('ffpb'):
  36. is_public = data.lstrip().lower().startswith("public:")
  37. if is_public and not (bot.config.ffpb.msg_target_public is None):
  38. data = data[7:].lstrip()
  39. target = bot.config.ffpb.msg_target_public
  40. elif not (bot.config.ffpb.msg_target is None):
  41. target = bot.config.ffpb.msg_target
  42. bot.msg(target, "[{0}] {1}".format(sender, str(data)))
  43. def _resolve_name (self, ip):
  44. """Resolves the host name of the given IP address
  45. and strips away the suffix (.infra)?.ffpb"""
  46. if ip.startswith ("127."):
  47. return "localhost"
  48. try:
  49. addr = dns.reversename.from_address (ip)
  50. return re.sub ("(.infra)?.ffpb.", "", str (ffpb_resolver.query (addr, "PTR")[0]))
  51. except dns.resolver.NXDOMAIN:
  52. return ip
  53. class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
  54. pass
  55. def setup(bot):
  56. global msgserver, peers_repo, alfred_method
  57. bot.memory['ffpb_in_setup'] = True
  58. # open highscores file (backed to filesystem)
  59. if 'highscores' in bot.memory and not bot.memory['highscores'] is None:
  60. bot.memory['highscores'].close()
  61. highscores = shelve.open('highscoredata', writeback=True)
  62. if not 'nodes' in highscores:
  63. highscores['nodes'] = 0
  64. highscores['nodes_ts'] = time.time()
  65. if not 'clients' in highscores:
  66. highscores['clients'] = 0
  67. highscores['clients_ts'] = time.time()
  68. bot.memory['highscores'] = highscores
  69. seen_nodes = shelve.open('nodes.seen', writeback=True)
  70. bot.memory['seen_nodes'] = seen_nodes
  71. # no need to configure anything else if the ffpb config section is missing
  72. if not bot.config.has_section('ffpb'):
  73. bot.memory['ffpb_in_setup'] = False
  74. return
  75. # open the git repository containing the peers files
  76. if not bot.config.ffpb.peers_directory is None:
  77. peers_repo = git.Repo(bot.config.ffpb.peers_directory)
  78. assert peers_repo.bare == False
  79. # if configured start the messaging server
  80. if int(bot.config.ffpb.msg_enable) == 1:
  81. host = "localhost"
  82. port = 2342
  83. if not bot.config.ffpb.msg_host is None: host = bot.config.ffpb.msg_host
  84. if not bot.config.ffpb.msg_port is None: port = int(bot.config.ffpb.msg_port)
  85. msgserver = ThreadingTCPServer((host,port), MsgHandler)
  86. msgserver.bot = bot
  87. ip, port = msgserver.server_address
  88. print("Messaging server listening on {}:{}".format(ip,port))
  89. msgserver_thread = threading.Thread(target=msgserver.serve_forever)
  90. msgserver_thread.daemon = True
  91. msgserver_thread.start()
  92. # initially fetch ALFRED data
  93. alfred_method = bot.config.ffpb.alfred_method
  94. ffpb_updatealfred(bot)
  95. bot.memory['ffpb_in_setup'] = False
  96. def shutdown(bot):
  97. global msgserver
  98. # store highscores
  99. if 'highscores' in bot.memory and not bot.memory['highscores'] is None:
  100. bot.memory['highscores'].sync()
  101. bot.memory['highscores'].close()
  102. del(bot.memory['highscores'])
  103. # store seen_nodes
  104. if 'seen_nodes' in bot.memory and bot.memory['seen_nodes'] != None:
  105. bot.memory['seen_nodes'].close()
  106. bot.memory['seen_nodes'] = None
  107. del(bot.memory['seen_nodes'])
  108. # shut down messaging server
  109. if not msgserver is None:
  110. msgserver.shutdown()
  111. print("Closed messaging server.")
  112. msgserver = None
  113. @willie.module.commands("help")
  114. @willie.module.commands("hilfe")
  115. @willie.module.commands("man")
  116. def ffpb_help(bot, trigger):
  117. """Meldet häufig benutzte Funktionen."""
  118. functions = {
  119. "!ping <knoten>": "Prüfe ob der Knoten erreichbar ist.",
  120. "!status": "Aktuellen Status des Netzwerks (insb. Anzahl Knoten und Clients) ausgegeben.",
  121. "!info <knoten>": "Allgemeine Information zu dem Knoten anzeigen.",
  122. "!link <knoten>": "MAC-Adresse und Link zur Status-Seite des Knotens anzeigen.",
  123. "!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)",
  124. }
  125. param = trigger.group(2)
  126. if param is None:
  127. bot.say("Funktionen: " + str.join(", ", sorted(functions.keys())))
  128. return
  129. if param.startswith("!"): param = param[1:]
  130. for fun in functions.keys():
  131. if fun.startswith("!" + param + " "):
  132. bot.say("Hilfe zu '" + fun + "': " + functions[fun])
  133. return
  134. bot.say("Allgemeine Hilfe gib t's mit !help - ohne Parameter.")
  135. def ffpb_findnode(bot, name):
  136. """helper: try to identify the node the user meant by the given name"""
  137. # no name, no node
  138. if name is None or len(name) == 0:
  139. return None
  140. name = str(name).strip()
  141. names = {}
  142. alfred_data = bot.memory['alfred_data'] if 'alfred_data' in bot.memory else None
  143. if not alfred_data is None:
  144. # try to match MAC in ALFRED data
  145. m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name)
  146. if (not m is None):
  147. mac = m.group(0).lower()
  148. if mac in alfred_data:
  149. return alfred_data[mac]
  150. # try to find alias MAC
  151. for nodeid in alfred_data:
  152. node = alfred_data[nodeid]
  153. if "network" in node:
  154. if "mac" in node["network"] and node["network"]["mac"].lower() == mac:
  155. return node
  156. if "mesh_interfaces" in node["network"]:
  157. for mim in node["network"]["mesh_interfaces"]:
  158. if mim.lower() == mac:
  159. return node
  160. return {
  161. 'hostname': '?-' + mac.replace(':','').lower(),
  162. 'network': { 'addresses': [ mac2ipv6(mac, 'fdca:ffee:ff12:132:') ], 'mac': mac, },
  163. 'hardware': { 'model': 'derived-from-mac' },
  164. }
  165. # look through the ALFRED peers
  166. for nodeid in alfred_data:
  167. node = alfred_data[nodeid]
  168. if 'hostname' in node:
  169. h = node['hostname']
  170. if h.lower() == name.lower():
  171. return node
  172. else:
  173. names[h] = nodeid
  174. # try peers_repo
  175. if not peers_repo is None:
  176. peer_name = None
  177. peer_mac = None
  178. peer_file = None
  179. for b in peers_repo.heads.master.commit.tree.blobs:
  180. if b.name.lower() == name.lower():
  181. peer_name = b.name
  182. peer_file = b.abspath
  183. break
  184. if (not peer_file is None) and os.path.exists(peer_file):
  185. peerfile = open(peer_file, "r")
  186. for line in peerfile:
  187. if line.startswith("# MAC:"):
  188. peer_mac = line[6:].strip()
  189. peerfile.close()
  190. if not (peer_mac is None):
  191. return {
  192. 'hostname': peer_name,
  193. 'network': { 'addresses': [ mac2ipv6(peer_mac, 'fdca:ffee:ff12:132:') ], 'mac': peer_mac },
  194. 'hardware': { 'model': 'derived-from-vpnkeys' },
  195. }
  196. # do a similar name lookup in the ALFRED data
  197. if not alfred_data is None:
  198. possibilities = difflib.get_close_matches(name, [ x for x in names ], cutoff=0.8)
  199. print('findnode: Fuzzy matching \'{0}\' got {1} entries: {2}'.format(name, len(possibilities), ', '.join(possibilities)))
  200. if len(possibilities) == 1:
  201. # if we got exactly one candidate that might be it
  202. return alfred_data[names[possibilities[0]]]
  203. # none of the above was able to identify the requested node
  204. return None
  205. def ffpb_get_alfreddata(bot, ensure_recent=True):
  206. """helper: return current ALFRED data (or None,
  207. if the data is outdated and ensure_recent is set)"""
  208. if not 'alfred_data' in bot.memory or bot.memory['alfred_data'] is None:
  209. return None
  210. if ensure_recent:
  211. # get timestamp of last ALFRED update (set by ffpb_updatealfred())
  212. alfred_update = bot.memory['alfred_update'] if 'alfred_update' in bot.memory else None
  213. if alfred_update is None: return None
  214. # data must not be older than 5 minutes
  215. timeout = datetime.datetime.now() - datetime.timedelta(minutes=5)
  216. is_outdated = timeout > alfred_update
  217. #print("ALFRED outdated? {0} (timeout={1} vs. lastupdate={2})".format(is_outdated, timeout, alfred_update))
  218. if is_outdated:
  219. return None
  220. return bot.memory['alfred_data']
  221. def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata = True):
  222. """helper: call ffpb_findnode() and give common answers via bot if nothing has been found"""
  223. if (name is None or len(name) == 0):
  224. bot.reply("Grün.")
  225. return None
  226. alfred_data = ffpb_get_alfreddata(bot, ensure_recent_alfreddata)
  227. if alfred_data is None and ensure_recent_alfreddata:
  228. bot.say("Ich habe gerade keine (aktuellen) Informationen, daher sage ich mal lieber nichts zu '" + name + "'.")
  229. return None
  230. node = ffpb_findnode(bot, name)
  231. if node is None:
  232. bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
  233. return node
  234. def mac2ipv6(mac, prefix=None):
  235. """Calculate IPv6 address from given MAC,
  236. optionally replacing the fe80:: prefix with a given one."""
  237. result = str(netaddr.EUI(mac).ipv6_link_local())
  238. if (not prefix is None) and (result.startswith("fe80::")):
  239. result = prefix + result[6:]
  240. return result
  241. @willie.module.interval(30)
  242. def ffpb_updatealfred(bot):
  243. """Aktualisiere ALFRED-Daten"""
  244. if alfred_method is None or alfred_method == "None":
  245. return
  246. alfred_data = None
  247. updated = None
  248. if alfred_method == "exec":
  249. rawdata = subprocess.check_output(['alfred-json', '-z', '-r', '158'])
  250. updated = datetime.datetime.now()
  251. elif alfred_method.startswith("http"):
  252. try:
  253. rawdata = urllib2.urlopen(alfred_method)
  254. except:
  255. print("Failed to download ALFRED data.")
  256. return
  257. updated = datetime.datetime.fromtimestamp(mktime_tz(rawdata.info().getdate_tz("Last-Modified")))
  258. else:
  259. print("Unknown ALFRED data method '", alfred_method, "', cannot load new data.", sep="")
  260. bot.memory['alfred_data'] = None
  261. return
  262. try:
  263. alfred_data = json.load(rawdata)
  264. #print("Fetched new ALFRED data:", len(alfred_data), "entries")
  265. except ValueError as e:
  266. print("Failed to parse ALFRED data: " + str(e))
  267. return
  268. bot.memory['alfred_data'] = alfred_data
  269. bot.memory['alfred_update'] = updated
  270. seen_nodes = bot.memory['seen_nodes'] if 'seen_nodes' in bot.memory else None
  271. if not seen_nodes is None:
  272. new = []
  273. for nodeid in alfred_data:
  274. nodeid = str(nodeid)
  275. if not nodeid in seen_nodes:
  276. seen_nodes[nodeid] = updated
  277. new.append((nodeid,alfred_data[nodeid]['hostname']))
  278. print('First time seen: ' + str(nodeid))
  279. if len(new) > 0 and not bot.memory['ffpb_in_setup']:
  280. action_msg = 'bemerkt '
  281. if len(new) == 1:
  282. action_msg += 'den neuen Knoten \'' + str(new[0][1]) + '\''
  283. else:
  284. action_msg += 'die neuen Knoten \'' + '\', \''.join([ str(x[1]) for x in new[0:-1] ]) + '\' und \'' + str(new[-1][1]) + '\''
  285. action_target = bot.config.ffpb.msg_target
  286. bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
  287. @willie.module.commands('debug-alfred')
  288. def ffpb_debug_alfred(bot, trigger):
  289. """Zeige Stand der Alfred-Daten an."""
  290. alfred_data = ffpb_get_alfreddata(bot)
  291. if alfred_data is None:
  292. bot.say("Keine ALFRED-Daten vorhanden.")
  293. else:
  294. bot.say("ALFRED Daten: count={0} lastupdate={1}".format(len(alfred_data), bot.memory['alfred_update'] if 'alfred_memory' in bot.memory else '?'))
  295. @willie.module.commands('alfred-data')
  296. def ffpb_peerdata(bot, trigger):
  297. """Zeige Daten zum angegebenen Node an."""
  298. # user must be a bot admin
  299. if (not trigger.admin):
  300. bot.say('I wont leak (possibly) sensitive data to you.')
  301. return
  302. # query must be a PM or as OP in the channel
  303. if (not trigger.is_privmsg) and (not trigger.nick in bot.ops[trigger.sender]):
  304. bot.say('Kein Keks? Keine Daten.')
  305. return
  306. # identify node or bail out
  307. target_name = trigger.group(2)
  308. node = ffpb_findnode_from_botparam(bot, target_name)
  309. if node is None: return
  310. # reply each key in the node's data
  311. for key in node:
  312. if key in [ 'hostname' ]: continue
  313. bot.say("{0}.{1} = {2}".format(node['hostname'], key, str(node[key])))
  314. @willie.module.interval(60)
  315. def ffpb_updatepeers(bot):
  316. """Aktualisiere die Knotenliste und melde das Diff"""
  317. if peers_repo is None:
  318. print('WARNING: peers_repo is None')
  319. return
  320. old_head = peers_repo.head.commit
  321. peers_repo.remotes.origin.pull()
  322. new_head = peers_repo.head.commit
  323. if new_head != old_head:
  324. print('git pull: from ' + str(old_head) + ' to ' + str(new_head))
  325. added = []
  326. changed = []
  327. renamed = []
  328. deleted = []
  329. for f in old_head.diff(new_head):
  330. if f.new_file:
  331. added.append(f.b_blob.name)
  332. elif f.deleted_file:
  333. deleted.append(f.a_blob.name)
  334. elif f.renamed:
  335. renamed.append([f.rename_from, f.rename_to])
  336. else:
  337. changed.append(f.a_blob.name)
  338. response = "Knoten-Update (VPN +{0} %{1} -{2}): ".format(len(added), len(renamed)+len(changed), len(deleted))
  339. for f in added:
  340. response += " +'{}'".format(f)
  341. for f in changed:
  342. response += " %'{}'".format(f)
  343. for f in renamed:
  344. response += " '{}'->'{}'".format(f[0],f[1])
  345. for f in deleted:
  346. response += " -'{}'".format(f)
  347. bot.msg(bot.config.ffpb.msg_target, response)
  348. def ffpb_fetch_stats(bot, url, memoryid):
  349. """Fetch a ffmap-style nodes.json from the given URL and
  350. store it in the bot's memory."""
  351. response = urllib2.urlopen(url)
  352. data = json.load(response)
  353. nodes_active = 0
  354. nodes_total = 0
  355. clients_count = 0
  356. for node in data['nodes']:
  357. if node['flags']['gateway'] or node['flags']['client']:
  358. continue
  359. nodes_total += 1
  360. if node['flags']['online']:
  361. nodes_active += 1
  362. if 'legacy' in node['flags'] and node['flags']['legacy']:
  363. clients_count -= 1
  364. for link in data['links']:
  365. if link['type'] == 'client':
  366. clients_count += 1
  367. if not memoryid in bot.memory:
  368. bot.memory[memoryid] = { }
  369. stats = bot.memory[memoryid]
  370. stats["fetchtime"] = time.time()
  371. stats["nodes_active"] = nodes_active
  372. stats["nodes_total"] = nodes_total
  373. stats["clients"] = clients_count
  374. return (nodes_active, nodes_total, clients_count)
  375. @willie.module.interval(15)
  376. def ffpb_get_stats(bot):
  377. """Hole aktuelle Statistik-Daten, falls sich der Highscore ändert melde dies."""
  378. highscores = bot.memory['highscores'] if 'highscores' in bot.memory else None
  379. if highscores is None:
  380. print('HIGHSCORE not in bot memory')
  381. return
  382. (nodes_active, nodes_total, clients_count) = ffpb_fetch_stats(bot, 'http://map.paderborn.freifunk.net/nodes.json', 'ffpb_stats')
  383. highscore_changed = False
  384. if nodes_active > highscores['nodes']:
  385. highscores['nodes'] = nodes_active
  386. highscores['nodes_ts'] = time.time()
  387. highscore_changed = True
  388. if clients_count > highscores['clients']:
  389. highscores['clients'] = clients_count
  390. highscores['clients_ts'] = time.time()
  391. highscore_changed = True
  392. if highscore_changed:
  393. print('HIGHSCORE changed: {0} nodes ({1}), {2} clients ({3})'.format(highscores['nodes'], highscores['nodes_ts'], highscores['clients'], highscores['clients_ts']))
  394. if not (bot.config.ffpb.msg_target is None):
  395. 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'])))
  396. action_target = bot.config.ffpb.msg_target
  397. if (not bot.config.ffpb.msg_target_public is None):
  398. action_target = bot.config.ffpb.msg_target_public
  399. bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
  400. def pretty_date(time=False):
  401. """
  402. Get a datetime object or a int() Epoch timestamp and return a
  403. pretty string like 'an hour ago', 'Yesterday', '3 months ago',
  404. 'just now', etc
  405. """
  406. from datetime import datetime
  407. now = datetime.now()
  408. compare = None
  409. if type(time) is int:
  410. compare = datetime.fromtimestamp(time)
  411. elif type(time) is float:
  412. compare = datetime.fromtimestamp(int(time))
  413. elif isinstance(time,datetime):
  414. compare = time
  415. elif not time:
  416. compare = now
  417. diff = now - compare
  418. second_diff = diff.seconds
  419. day_diff = diff.days
  420. if day_diff < 0:
  421. return ''
  422. if day_diff == 0:
  423. if second_diff < 10:
  424. return "gerade eben"
  425. if second_diff < 60:
  426. return "vor " + str(second_diff) + " Sekunden"
  427. if second_diff < 120:
  428. return "vor einer Minute"
  429. if second_diff < 3600:
  430. return "vor " + str(second_diff / 60) + " Minuten"
  431. if second_diff < 7200:
  432. return "vor einer Stunde"
  433. if second_diff < 86400:
  434. return "vor " + str(second_diff / 3600) + " Stunden"
  435. if day_diff == 1:
  436. return "gestern"
  437. if day_diff < 7:
  438. return "vor " + str(day_diff) + " Tagen"
  439. return "am " + compare.strftime('%d.%m.%Y um %H:%M Uhr')
  440. @willie.module.commands('ping')
  441. def ffpb_ping(bot, trigger=None, target_name=None):
  442. """Ping an Knoten"""
  443. if target_name is None: target_name = trigger.group(2)
  444. node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
  445. if node is None: return None
  446. target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
  447. target_alias = node["hostname"]
  448. print("pinging '{0}' at {1} ...".format(target_name, target))
  449. result = os.system('ping6 -c 2 -W 1 ' + target + ' >/dev/null')
  450. if result == 0:
  451. print("ping to '{0}' succeeded".format(target_name))
  452. if not bot is None: bot.say('Knoten "' + target_alias + '" antwortet \o/')
  453. return True
  454. elif result == 1 or result == 256:
  455. print("ping to '{0}' failed".format(target_name))
  456. if not bot is None: bot.say('Keine Antwort von "' + target_alias + '" :-(')
  457. return False
  458. else:
  459. print("ping to '{0}' broken: result='{1}'".format(target_name, result))
  460. if not bot is None: bot.say('Uh oh, irgendwas ist kaputt. Chef, ping result = ' + str(result) + ' - darf ich das essen?')
  461. return None
  462. @willie.module.commands('exec-on-peer')
  463. def ffpb_remoteexec(bot, trigger):
  464. """Remote Execution für Knoten (mit SSH-Key des Bots)"""
  465. bot_params = trigger.group(2).split(' ',1)
  466. if len(bot_params) != 2:
  467. bot.say('Wenn du nicht sagst wo mach ich remote execution bei dir!')
  468. bot.say('Tipp: !exec-on-peer <peer> <cmd>')
  469. return
  470. target_name = bot_params[0]
  471. target_cmd = bot_params[1]
  472. # remote execution may only be trigger by bot admins
  473. if not trigger.admin:
  474. bot.say('I can haz sudo?')
  475. return
  476. # make sure remote execution is done in public
  477. if trigger.is_privmsg:
  478. bot.say('Bitte per Channel.')
  479. return
  480. # double-safety: user must be op in the channel, too (hoping for NickServ authentication)
  481. if not trigger.nick in bot.ops[trigger.sender]:
  482. bot.say('Geh weg.')
  483. return
  484. # identify requested node or bail out
  485. node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
  486. if node is None: return
  487. # use the node's first non-linklocal address
  488. target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
  489. target_alias = node["hostname"]
  490. # assemble SSH command
  491. cmd = 'ssh -6 -l root ' + target + ' -- "' + target_cmd + '"'
  492. print("REMOTE EXEC = " + cmd)
  493. try:
  494. # call SSH
  495. result = subprocess.check_output(['ssh', '-6n', '-l', 'root', '-o', 'BatchMode=yes', '-o','StrictHostKeyChecking=no', target, target_cmd], stderr=subprocess.STDOUT, shell=False)
  496. # fetch results and send at most 8 of them as response
  497. lines = str(result).splitlines()
  498. if len(lines) == 0:
  499. bot.say('exec-on-peer(' + target_alias + '): No output')
  500. return
  501. msg = 'exec-on-peer(' + target_alias + '): ' + str(len(lines)) + ' Zeilen'
  502. if len(lines) > 8:
  503. msg += ' (zeige max. 8)'
  504. bot.say(msg + ':')
  505. for line in lines[0:8]:
  506. bot.say(line)
  507. except subprocess.CalledProcessError as e:
  508. bot.say('Fehler '+str(e.returncode)+' bei exec-on-peer('+target_alias+'): ' + e.output)