123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882 |
- # -*- coding: utf-8 -*-
- from __future__ import print_function
- import willie
- from datetime import datetime, timedelta
- import difflib
- from email.utils import mktime_tz
- from fnmatch import fnmatch
- import git
- import netaddr
- import json
- import urllib2
- import re
- import os
- import random
- import shelve
- import subprocess
- import time
- import dns.resolver, dns.reversename
- import SocketServer
- import threading
- msgserver = None
- peers_repo = None
- nodeaccess = None
- alfred_method = None
- ffpb_resolver = dns.resolver.Resolver ()
- ffpb_resolver.nameservers = ['10.132.254.53']
- class MsgHandler(SocketServer.BaseRequestHandler):
- """Reads line from TCP stream and forwards it to configured IRC channels."""
- def handle(self):
- data = self.request.recv(2048).strip()
- sender = self.resolve_name(self.client_address[0])
- bot = self.server.bot
- if bot is None:
- print("ERROR: No bot in handle() :-(")
- return
- target = bot.config.core.owner
- if bot.config.has_section('ffpb'):
- is_public = data.lstrip().lower().startswith("public:")
- if is_public and not bot.config.ffpb.msg_target_public is None:
- data = data[7:].lstrip()
- target = bot.config.ffpb.msg_target_public
- elif not bot.config.ffpb.msg_target is None:
- target = bot.config.ffpb.msg_target
- bot.msg(target, "[{0}] {1}".format(sender, str(data)))
- def resolve_name(self, ipaddr):
- """
- Resolves the host name of the given IP address
- and strips away the suffix (.infra)?.ffpb
- """
- if ipaddr.startswith("127."):
- return "localhost"
- try:
- addr = dns.reversename.from_address(ipaddr)
- return re.sub("(.infra)?.ffpb.", "", str(ffpb_resolver.query(addr, "PTR")[0]))
- except dns.resolver.NXDOMAIN:
- return ipaddr
- class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
- """Defines a threaded TCP socket server."""
- bot = None
- def setup(bot):
- """Called by willie upon loading this plugin."""
- global msgserver, peers_repo, alfred_method, nodeaccess
- # signal begin of setup routine
- bot.memory['ffpb_in_setup'] = True
- # load list of seen nodes from disk
- seen_nodes = shelve.open('nodes.seen', writeback=True)
- bot.memory['seen_nodes'] = seen_nodes
- # load list of node ACL from disk (used in playitsafe())
- nodeaccess = shelve.open('nodes.acl', writeback=True)
- # no need to configure anything else if the ffpb config section is missing
- if not bot.config.has_section('ffpb'):
- bot.memory['ffpb_in_setup'] = False
- return
- # open the git repository containing the peers files
- if not bot.config.ffpb.peers_directory is None:
- peers_repo = git.Repo(bot.config.ffpb.peers_directory)
- assert peers_repo.bare == False
- # if configured, start the messaging server
- if int(bot.config.ffpb.msg_enable) == 1:
- host = "localhost"
- port = 2342
- if not bot.config.ffpb.msg_host is None:
- host = bot.config.ffpb.msg_host
- if not bot.config.ffpb.msg_port is None:
- port = int(bot.config.ffpb.msg_port)
- msgserver = ThreadingTCPServer((host, port), MsgHandler)
- msgserver.bot = bot
- ipaddr, port = msgserver.server_address
- print("Messaging server listening on {}:{}".format(ipaddr, port))
- msgserver_thread = threading.Thread(target=msgserver.serve_forever)
- msgserver_thread.daemon = True
- msgserver_thread.start()
- # initially fetch ALFRED data
- alfred_method = bot.config.ffpb.alfred_method
- if not 'alfred_data' in bot.memory:
- bot.memory['alfred_data'] = {}
- if not 'alfred_update' in bot.memory:
- bot.memory['alfred_update'] = datetime(1970, 1, 1, 23, 42)
- ffpb_updatealfred(bot)
- # signal end of setup routine
- bot.memory['ffpb_in_setup'] = False
- def shutdown(bot):
- global msgserver, nodeaccess
- # store node acl
- if not nodeaccess is None:
- nodeaccess.sync()
- nodeaccess.close()
- nodeaccess = None
- # store seen nodes
- if 'seen_nodes' in bot.memory and bot.memory['seen_nodes'] != None:
- bot.memory['seen_nodes'].close()
- bot.memory['seen_nodes'] = None
- del bot.memory['seen_nodes']
- # shutdown messaging server
- if not msgserver is None:
- msgserver.shutdown()
- print("Closed messaging server.")
- msgserver = None
- @willie.module.commands("help")
- @willie.module.commands("hilfe")
- @willie.module.commands("man")
- def ffpb_help(bot, trigger):
- """Display commony ulsed functions."""
- functions = {
- "!ping <knoten>": "Prüfe ob der Knoten erreichbar ist.",
- "!status": "Aktuellen Status des Netzwerks (insb. Anzahl Knoten und Clients) ausgegeben.",
- "!info <knoten>": "Allgemeine Information zu dem Knoten anzeigen.",
- "!link <knoten>": "MAC-Adresse und Link zur Status-Seite des Knotens anzeigen.",
- "!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)",
- "!mesh <knoten>": "Zeige Mesh-Partner eines Knotens",
- }
- param = trigger.group(2)
- if param is None:
- bot.say("Funktionen: " + str.join(", ", sorted(functions.keys())))
- return
- if param.startswith("!"):
- param = param[1:]
- for fun in functions.keys():
- if fun.startswith("!" + param + " "):
- bot.say("Hilfe zu '" + fun + "': " + functions[fun])
- return
- bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
- def playitsafe(bot, trigger,
- botadmin=False, admin_channel=False, via_channel=False, via_privmsg=False, need_op=False, node=None,
- reply_directly=True, debug_user=None, debug_ignorebotadmin=False):
- """
- helper: checks that the triggering user has the necessary rights
- Returns true if everything is okay.
- If it's not, a reply is send via the bot and false is returned.
- """
- if via_channel and via_privmsg:
- raise Exception('Der Entwickler ist ein dummer, dummer Junge ' +
- '(playitsafe hat via_channel + via_privmsg gleichzeitig gesetzt).')
- user = trigger.nick if debug_user is None else debug_user
- user = user.lower()
- # botadmin: you need to be configured as a bot admin
- if botadmin and not trigger.admin:
- if reply_directly:
- bot.say('Du brauchst Super-Kuh-Kräfte um dieses Kommando auszuführen.')
- return False
- # via_channel: the request must not be a private conversation
- if via_channel and trigger.is_privmsg:
- if reply_directly:
- bot.say('Bitte per Channel - mehr Transparenz wagen und so!')
- return False
- # via_privmsg: the request must be a private conversation
- if via_privmsg and not trigger.is_privmsg:
- if reply_directly:
- bot.say('Solche Informationen gibt es nur per PM, da bin ich ja schon ein klein wenig sensibel ...')
- return False
- # need_op: if the message is in a channel, check that the user has OP there
- if need_op and (not trigger.is_privmsg) and (not user in bot.ops[trigger.sender]):
- if reply_directly:
- bot.say('Keine Zimtschnecke, keine Kekse.')
- return False
- # node: check that the user is whitelisted (or is admin)
- if not node is None and (debug_ignorebotadmin or not trigger.admin):
- acluser = [x for x in nodeaccess if x.lower() == user]
- acluser = acluser[0] if len(acluser) == 1 else None
- if nodeaccess is None or acluser is None:
- if reply_directly:
- bot.reply('You! Shall! Not! Access!')
- return False
- nodeid = node['node_id'] if 'node_id' in node else None
- matched = False
- for x in nodeaccess[acluser]:
- if x == nodeid or fnmatch(node['hostname'], x):
- matched = True
- break
- if not matched:
- if reply_directly:
- bot.reply('Mach das doch bitte auf deinen Knoten, kthxbye.')
- return False
- return True
- @willie.module.commands('nodeacl')
- def ffpb_nodeacl(bot, trigger):
- """Configure ACL for nodes."""
- if not playitsafe(bot, trigger, botadmin=True):
- # the check function already gives a bot reply, just exit here
- return
- # ensure the user gave arguments
- if trigger.group(2) is None or len(trigger.group(2)) == 0:
- bot.say('Sag doch was du willst ... einmal mit Profis arbeiten, ey -.-')
- return
- # read additional arguments
- cmd = trigger.group(3).lower()
- if cmd == 'list':
- user = trigger.group(4)
- if user is None:
- usernames = [x for x in nodeaccess]
- bot.say('ACLs gesetzt für die User: ' + ', '.join(usernames))
- return
- user = user.lower()
- uid = [x for x in nodeaccess if x.lower() == user]
- if len(uid) == 0:
- bot.say('Für \'{0}\' ist keine Node ACL gesetzt.'.format(user))
- return
- bot.say('Node ACL für \'{0}\' = \'{1}\''.format(
- uid[0],
- '\', \''.join(nodeaccess[uid[0]]))
- )
- return
- if cmd in ['add', 'del', 'check']:
- user = trigger.group(4)
- value = trigger.group(5)
- if user is None or value is None:
- bot.say('Du bist eine Pappnase - User und Knoten, bitte.')
- return
- user = str(user)
- print('NodeACL ' + cmd + ' \'' + value + '\' for user \'' + user + '\'')
- uid = [x for x in nodeaccess if x == user or x.lower() == user]
- if cmd == 'add':
- uid = uid[0] if len(uid) > 0 else user
- if not uid in nodeaccess:
- nodeaccess[uid] = []
- if not value in nodeaccess[uid]:
- nodeaccess[uid].append(value)
- bot.say('201 nodeACL \'{0}\' +\'{1}\''.format(uid, value))
- else:
- bot.say('304 nodeACL \'{0}\' contains \'{1}\''.format(uid, value))
- elif cmd == 'del':
- if len(uid) == 0:
- bot.say('404 nodeACL \'{0}\''.format(uid))
- return
- if value in nodeaccess[uid]:
- nodeaccess[uid].remove(value)
- bot.say('200 nodeACL \'{0}\' -\'{1}\''.format(uid, value))
- else:
- bot.say('404 nodeACL \'{0}\' does not contain \'{1}\''.format(uid, value))
- elif cmd == 'check':
- if len(uid) == 0:
- bot.say('Nope, keine ACL gesetzt.')
- return
- node = ffpb_findnode(value)
- if node is None:
- bot.say('Nope, kein Plan was für ein Knoten das ist.')
- return
- result = playitsafe(bot, trigger, debug_user=uid[0], debug_ignorebotadmin=True, node=node, reply_directly=False)
- if result == True:
- bot.say('Jupp.')
- elif result == False:
- bot.say('Nope.')
- else:
- bot.say('Huh? result=' + str(result))
- return
- bot.say('Unbekanntes Kommando. Probier "list [user]", "add user value" oder "del user value". Value kann node_id oder hostname-Maske sein.')
- def ffpb_ensurenodeid(nodedata):
- """Makes sure that the given dict has a 'node_id' field."""
- if 'node_id' in nodedata:
- return nodedata
- # derive node's id
- nodeid = nodedata['network']['mac'].replace(':', '') if 'network' in nodedata and 'mac' in nodedata['network'] else None
- # assemble extended data
- result = {'node_id': nodeid}
- for key in nodedata:
- result[key] = nodedata[key]
- return result
- def ffpb_findnode(name, alfred_data=None, allow_fuzzymatching=True):
- """helper: try to identify the node the user meant by the given name"""
- # no name, no node
- if name is None or len(name) == 0:
- return None
- name = str(name).strip()
- # disable fuzzy matching if name is enclosed in quotes
- if name.startswith('\'') and name.endswith('\'') or \
- name.startswith('"') and name.endswith('"'):
- name = name[1:-1]
- allow_fuzzymatching = False
- names = {}
- if not alfred_data is None:
- # try to match MAC
- m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name)
- if not m is None:
- mac = m.group(0).lower()
- if mac in alfred_data:
- return ffpb_ensurenodeid(alfred_data[mac])
- # try to find alias MAC in ALFRED data
- for nodeid in alfred_data:
- node = alfred_data[nodeid]
- if "network" in node:
- if "mac" in node["network"] and node["network"]["mac"].lower() == mac:
- return ffpb_ensurenodeid(node)
- if "mesh_interfaces" in node["network"]:
- for mim in node["network"]["mesh_interfaces"]:
- if mim.lower() == mac:
- return ffpb_ensurenodeid(node)
- nodeid = mac.replace(':', '').lower()
- return {
- 'nodeid': nodeid,
- 'hostname': '?-' + nodeid,
- 'network': {
- 'addresses': [mac2ipv6(mac, 'fdca:ffee:ff12:132:')],
- 'mac': mac,
- },
- 'hardware': {
- 'model': 'derived-from-mac',
- },
- }
- # look through the ALFRED peers
- for nodeid in alfred_data:
- node = alfred_data[nodeid]
- if 'hostname' in node:
- h = node['hostname']
- if h.lower() == name.lower():
- return node
- else:
- names[h] = nodeid
- # not found in ALFRED data -> try peers_repo
- if not peers_repo is None:
- peer_name = None
- peer_mac = None
- peer_file = None
- for b in peers_repo.heads.master.commit.tree.blobs:
- if b.name.lower() == name.lower():
- peer_name = b.name
- peer_file = b.abspath
- break
- if (not peer_file is None) and os.path.exists(peer_file):
- peerfile = open(peer_file, "r")
- for line in peerfile:
- if line.startswith("# MAC:"):
- peer_mac = line[6:].strip()
- peerfile.close()
- if not peer_mac is None:
- return {
- 'node_id': peer_mac.replace(':', ''),
- 'hostname': peer_name,
- 'network': {
- 'addresses': [mac2ipv6(peer_mac, 'fdca:ffee:ff12:132:'),],
- 'mac': peer_mac,
- },
- 'hardware': {
- 'model': 'derived-from-vpnkeys',
- },
- }
- # do a similar name lookup in the ALFRED data
- if allow_fuzzymatching and not alfred_data is None:
- allnames = [x for x in names]
- possibilities = difflib.get_close_matches(name, allnames, cutoff=0.75)
- print('findnode: Fuzzy matching \'{0}\' got {1} entries: {2}'.format(
- name,
- len(possibilities), ', '.join(possibilities))
- )
- if len(possibilities) == 1:
- # if we got exactly one candidate that might be it
- return ffpb_ensurenodeid(alfred_data[names[possibilities[0]]])
- # none of the above was able to identify the requested node
- return None
- def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata=True):
- """helper: call ffpb_findnode() and give common answers via bot if nothing has been found"""
- if name is None or len(name) == 0:
- if not bot is None:
- bot.reply("Grün.")
- return None
- alfred_data = get_alfred_data(bot, ensure_recent_alfreddata)
- if ensure_recent_alfreddata and alfred_data is None:
- if not bot is None:
- bot.say('Informationen sind ausverkauft bzw. veraltet, ' +
- 'daher sage ich mal lieber nichts zu \'' + name + '\'.')
- return None
- node = ffpb_findnode(name, alfred_data)
- if node is None:
- if not bot is None:
- bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
- return node
- def mac2ipv6(mac, prefix=None):
- """Calculate IPv6 address from given MAC,
- optionally replacing the fe80:: prefix with a given one."""
- result = str(netaddr.EUI(mac).ipv6_link_local())
- if (not prefix is None) and (result.startswith("fe80::")):
- result = prefix + result[6:]
- return result
- @willie.module.interval(30)
- def ffpb_updatealfred(bot):
- """Aktualisiere ALFRED-Daten"""
- if alfred_method is None or alfred_method == "None":
- return
- updated = None
- if alfred_method == "exec":
- rawdata = subprocess.check_output(['alfred-json', '-z', '-r', '158'])
- updated = datetime.now()
- elif alfred_method.startswith("http"):
- try:
- rawdata = urllib2.urlopen(alfred_method)
- except urllib2.URLError as err:
- print("Failed to download ALFRED data:" + str(err))
- return
- last_modified = rawdata.info().getdate_tz("Last-Modified")
- updated = datetime.fromtimestamp(mktime_tz(last_modified))
- else:
- print("Unknown ALFRED data method '{0}', cannot load new data.".format(alfred_method))
- alfred_data = None
- return
- try:
- alfred_data = json.load(rawdata)
- #print("Fetched new ALFRED data:", len(alfred_data), "entries")
- except ValueError as err:
- print("Failed to parse ALFRED data: " + str(err))
- return
- bot.memory['alfred_data'] = alfred_data
- bot.memory['alfred_update'] = updated
- seen_nodes = bot.memory['seen_nodes'] if 'seen_nodes' in bot.memory else None
- if not seen_nodes is None:
- new = []
- for nodeid in alfred_data:
- nodeid = str(nodeid)
- if not nodeid in seen_nodes:
- seen_nodes[nodeid] = updated
- new.append((nodeid, alfred_data[nodeid]['hostname']))
- print('First time seen: ' + str(nodeid))
- if len(new) > 0 and not bot.memory['ffpb_in_setup']:
- action_msg = None
- if len(new) == 1:
- action_msg = random.choice((
- 'bemerkt den neuen Knoten {0}',
- 'entdeckt {0}',
- 'reibt sich die Augen und erblickt einen verpackungsfrischen Knoten {0}',
- u'heißt {0} im Mesh willkommen',
- 'freut sich, dass {0} aufgetaucht ist',
- 'traut seinen Augen kaum. {0} sagt zum ersten Mal: Hallo Freifunk Paderborn',
- u'sieht die ersten Herzschläge von {0}',
- u'stellt einen großen Pott Heißgetränk zu {0} und fragt ob es hier Meshpartner gibt.',
- )).format('\'' + str(new[0][1]) + '\'')
- else:
- action_msg = random.choice((
- 'bemerkt die neuen Knoten {0} und {1}',
- 'hat {0} und {1} entdeckt',
- 'bewundert {0} sowie {1}',
- 'freut sich, dass {0} und {1} nun auch online sind',
- u'heißt {0} und {1} im Mesh willkommen',
- 'fragt sich ob die noch jungen Herzen von {0} und {1} synchron schlagen',
- ))
- all_but_last = [str(x[1]) for x in new[0:-1]]
- last = str(new[-1][1])
- action_msg = action_msg.format(
- '\'' + '\', \''.join(all_but_last) + '\'',
- '\'' + last + '\''
- )
- action_target = bot.config.ffpb.msg_target
- if not bot.config.ffpb.msg_target_public is None:
- action_target = bot.config.ffpb.msg_target_public
- bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
- def get_alfred_data(bot, ensure_not_outdated=True):
- """
- Retrieves the stored alfred_data and optionally checks
- that it has been updated no more than 5 minutes ago.
- """
- alfred_data = bot.memory['alfred_data'] if 'alfred_data' in bot.memory else None
- alfred_update = bot.memory['alfred_update'] if 'alfred_update' in bot.memory else 0
- if alfred_data is None:
- return None
- if ensure_not_outdated:
- timeout = datetime.now() - timedelta(minutes=5)
- is_outdated = timeout > alfred_update
- if is_outdated:
- return None
- return alfred_data
- def ffpb_get_batcave_nodefield(nodeid, field):
- """Query the given field for the given nodeid from the BATCAVE."""
- raw_data = None
- try:
- # query BATCAVE for node's field
- raw_data = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/node/{0}/{1}'.format(nodeid, field))
- except urllib2.URLError as err:
- print('Failed to contact BATCAVE for \'{0}\'->\'{1}\': {2}'.format(nodeid, field, err))
- return None
- try:
- return json.load(raw_data)
- except ValueError as err:
- print('Could not parse BATCAVE\'s response as JSON for \'{0}\'->\'{1}\':'.format(nodeid, field, err))
- return None
- @willie.module.commands('debug-alfred')
- def ffpb_debug_alfred(bot, trigger):
- """Show statistics of available ALFRED data."""
- alfred_data = get_alfred_data(bot)
- if alfred_data is None:
- bot.say("Keine ALFRED-Daten vorhanden.")
- else:
- bot.say("ALFRED Daten: count={0} lastupdate={1}".format(len(alfred_data), bot.memory['alfred_update']))
- @willie.module.interval(60)
- def ffpb_updatepeers(bot):
- """Refresh list of peers and message the diff."""
- if peers_repo is None:
- print('WARNING: peers_repo is None')
- return
- old_head = peers_repo.head.commit
- peers_repo.remotes.origin.pull()
- new_head = peers_repo.head.commit
- if new_head != old_head:
- print('git pull: from ' + str(old_head) + ' to ' + str(new_head))
- added = []
- changed = []
- renamed = []
- deleted = []
- for f in old_head.diff(new_head):
- if f.new_file:
- added.append(f.b_blob.name)
- elif f.deleted_file:
- deleted.append(f.a_blob.name)
- elif f.renamed:
- renamed.append([f.rename_from, f.rename_to])
- else:
- changed.append(f.a_blob.name)
- response = "Knoten-Update (VPN +{0} %{1} -{2}): ".format(len(added), len(renamed)+len(changed), len(deleted))
- for f in added:
- response += " +'{}'".format(f)
- for f in changed:
- response += " %'{}'".format(f)
- for f in renamed:
- response += " '{}'->'{}'".format(f[0], f[1])
- for f in deleted:
- response += " -'{}'".format(f)
- bot.msg(bot.config.ffpb.msg_target, response)
- def ffpb_fetch_stats(bot, url, memoryid):
- """Fetch a ffmap-style nodes.json from the given URL and
- store it in the bot's memory."""
- response = urllib2.urlopen(url)
- data = json.load(response)
- nodes_active = 0
- nodes_total = 0
- clients_count = 0
- for node in data['nodes']:
- if node['flags']['gateway'] or node['flags']['client']:
- continue
- nodes_total += 1
- if node['flags']['online']:
- nodes_active += 1
- if 'legacy' in node['flags'] and node['flags']['legacy']:
- clients_count -= 1
- for link in data['links']:
- if link['type'] == 'client':
- clients_count += 1
- if not memoryid in bot.memory:
- bot.memory[memoryid] = {}
- stats = bot.memory[memoryid]
- stats["fetchtime"] = time.time()
- stats["nodes_active"] = nodes_active
- stats["nodes_total"] = nodes_total
- stats["clients"] = clients_count
- return (nodes_active, nodes_total, clients_count)
- def pretty_date(timestamp=False):
- """
- Get a datetime object or a int() Epoch timestamp and return a
- pretty string like 'an hour ago', 'Yesterday', '3 months ago',
- 'just now', etc
- """
- now = datetime.now()
- compare = None
- if type(timestamp) is int:
- compare = datetime.fromtimestamp(timestamp)
- elif type(timestamp) is float:
- compare = datetime.fromtimestamp(int(timestamp))
- elif isinstance(timestamp, datetime):
- compare = timestamp
- elif not timestamp:
- compare = now
- diff = now - compare
- second_diff = diff.seconds
- day_diff = diff.days
- if day_diff < 0:
- return ''
- if day_diff == 0:
- if second_diff < 10:
- return "gerade eben"
- if second_diff < 60:
- return "vor " + str(second_diff) + " Sekunden"
- if second_diff < 120:
- return "vor einer Minute"
- if second_diff < 3600:
- return "vor " + str(second_diff / 60) + " Minuten"
- if second_diff < 7200:
- return "vor einer Stunde"
- if second_diff < 86400:
- return "vor " + str(second_diff / 3600) + " Stunden"
- if day_diff == 1:
- return "gestern"
- if day_diff < 7:
- return "vor " + str(day_diff) + " Tagen"
- return "am " + compare.strftime('%d.%m.%Y um %H:%M Uhr')
- @willie.module.commands('ping')
- def ffpb_ping(bot, trigger=None, target_name=None, reply_directly=True):
- """Ping the given node"""
- # identify node or bail out
- if target_name is None:
- target_name = trigger.group(2)
- node = ffpb_findnode_from_botparam(bot, target_name,
- ensure_recent_alfreddata=False)
- if node is None:
- return None
- # get the first non-linklocal address from the node
- target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
- target_alias = node["hostname"]
- # execute the actual ping and reply the result
- print("pinging '{0}' at {1} ...".format(target_name, target))
- result = os.system('ping6 -c 2 -W 1 ' + target + ' >/dev/null')
- if result == 0:
- print("ping to '{0}' succeeded".format(target_name))
- if reply_directly:
- bot.say('Knoten "' + target_alias + '" antwortet \\o/')
- return True
- elif result == 1 or result == 256:
- print("ping to '{0}' failed".format(target_name))
- if reply_directly:
- bot.say('Keine Antwort von "' + target_alias + '" :-(')
- return False
- else:
- print("ping to '{0}' broken: result='{1}'".format(target_name, result))
- if reply_directly:
- bot.say('Uh oh, irgendwas ist kaputt. Chef, ping result = ' + str(result) + ' - darf ich das essen?')
- return None
- @willie.module.commands('mesh')
- def ffpb_nodemesh(bot, trigger):
- """Display mesh partners of the given node."""
- # identify node or bail out
- target_name = trigger.group(2)
- node = ffpb_findnode_from_botparam(bot, target_name,
- ensure_recent_alfreddata=False)
- if node is None:
- return None
- # derive node's id
- nodeid = node['node_id'] if 'node_id' in node else None
- if nodeid is None:
- msg = 'Mist, ich habe gerade den Zettel verlegt auf dem die Node-ID von \'{0}\' steht, bitte frag später noch einmal.'
- bot.say(msg.format(node['hostname'] if 'hostname' in node else target_name))
- return
- # query BATCAVE for node's neighbours (result is a list of MAC addresses)
- cave_result = ffpb_get_batcave_nodefield(nodeid, 'neighbours')
- # query BATCAVE for neighbour's names
- data = '&'.join([str(n) for n in cave_result])
- req = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/idmac2name', data)
- # filter out duplicate names
- neighbours = set()
- for line in req:
- ident, name = line.strip().split('=')
- neighbours.add(name)
- neighbours = [x for x in neighbours]
- # respond to the user
- if len(neighbours) == 0:
- bot.say(u'{0} hat keinen Mesh-Partner *schnüff*'.format(node['hostname']))
- elif len(neighbours) == 1:
- bot.say(u'{0} mesht mit \'{1}\''.format(node['hostname'], neighbours[0]))
- else:
- bot.say('{0} mesht mit \'{1}\' und \'{2}\''.format(node['hostname'], '\', \''.join(neighbours[:-1]), neighbours[-1]))
- @willie.module.commands('exec-on-peer')
- def ffpb_remoteexec(bot, trigger):
- """Remote execution on the given node"""
- bot_params = trigger.group(2).split(' ', 1) if trigger.group(2) is not None else []
- if len(bot_params) != 2:
- bot.say('Wenn du nicht sagst wo mach ich remote execution bei dir!')
- bot.say('Tipp: !exec-on-peer <peer> <cmd>')
- return
- target_name = bot_params[0]
- target_cmd = bot_params[1]
- # identify requested node or bail out
- node = ffpb_findnode_from_botparam(bot, target_name,
- ensure_recent_alfreddata=False)
- if node is None:
- return
- # check ACL
- if not playitsafe(bot, trigger, via_channel=True, node=node):
- return
- # use the node's first non-linklocal address
- naddrs = node["network"]["addresses"]
- target = [x for x in naddrs if not x.lower().startswith("fe80:")][0]
- target_alias = node["hostname"]
- # assemble SSH command
- cmd = [
- 'ssh',
- '-6n',
- '-l', 'root',
- '-o', 'BatchMode=yes',
- '-o', 'StrictHostKeyChecking=no',
- target,
- target_cmd,
- ]
- print("REMOTE EXEC = " + str(cmd))
- try:
- # call SSH
- result = subprocess.check_output(
- cmd,
- stderr=subprocess.STDOUT,
- shell=False,
- )
- # fetch results and sent at most 8 of them as response
- lines = str(result).splitlines()
- if len(lines) == 0:
- bot.say('exec-on-peer(' + target_alias + '): No output')
- return
- msg = 'exec-on-peer({0}): {1} Zeilen'.format(target_alias, len(lines))
- if len(lines) > 8:
- msg += ' (zeige max. 8)'
- bot.say(msg + ':')
- for line in lines[0:8]:
- bot.say(line)
- except subprocess.CalledProcessError as err:
- bot.say('Fehler {0} bei exec-on-peer({1}): {2}'.format(
- err.returncode,
- target_alias,
- err.output
- ))
|