|
@@ -34,6 +34,8 @@ ffpb_resolver = dns.resolver.Resolver ()
|
|
ffpb_resolver.nameservers = ['10.132.254.53']
|
|
ffpb_resolver.nameservers = ['10.132.254.53']
|
|
|
|
|
|
class MsgHandler(SocketServer.BaseRequestHandler):
|
|
class MsgHandler(SocketServer.BaseRequestHandler):
|
|
|
|
+ """Reads line from TCP stream and forwards it to configured IRC channels."""
|
|
|
|
+
|
|
def handle(self):
|
|
def handle(self):
|
|
data = self.request.recv(2048).strip()
|
|
data = self.request.recv(2048).strip()
|
|
sender = self._resolve_name (self.client_address[0])
|
|
sender = self._resolve_name (self.client_address[0])
|
|
@@ -56,6 +58,9 @@ class MsgHandler(SocketServer.BaseRequestHandler):
|
|
bot.msg(target, "[{0}] {1}".format(sender, str(data)))
|
|
bot.msg(target, "[{0}] {1}".format(sender, str(data)))
|
|
|
|
|
|
def _resolve_name (self, ip):
|
|
def _resolve_name (self, ip):
|
|
|
|
+ """Resolves the host name of the given IP address
|
|
|
|
+ and strips away the suffix (.infra)?.ffpb"""
|
|
|
|
+
|
|
if ip.startswith ("127."):
|
|
if ip.startswith ("127."):
|
|
return "localhost"
|
|
return "localhost"
|
|
|
|
|
|
@@ -71,8 +76,10 @@ class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
|
|
def setup(bot):
|
|
def setup(bot):
|
|
global msgserver, peers_repo, alfred_method, highscores, monitored_nodes
|
|
global msgserver, peers_repo, alfred_method, highscores, monitored_nodes
|
|
|
|
|
|
|
|
+ # signal begin of setup routine
|
|
bot.memory['ffpb_in_setup'] = True
|
|
bot.memory['ffpb_in_setup'] = True
|
|
|
|
|
|
|
|
+ # load highscores from disk
|
|
highscores = shelve.open('highscoredata', writeback=True)
|
|
highscores = shelve.open('highscoredata', writeback=True)
|
|
if not 'nodes' in highscores:
|
|
if not 'nodes' in highscores:
|
|
highscores['nodes'] = 0
|
|
highscores['nodes'] = 0
|
|
@@ -81,19 +88,24 @@ def setup(bot):
|
|
highscores['clients'] = 0
|
|
highscores['clients'] = 0
|
|
highscores['clients_ts'] = time.time()
|
|
highscores['clients_ts'] = time.time()
|
|
|
|
|
|
|
|
+ # load list of monitored nodes and their last status from disk
|
|
monitored_nodes = shelve.open('nodes.monitored', writeback=True)
|
|
monitored_nodes = shelve.open('nodes.monitored', writeback=True)
|
|
|
|
|
|
|
|
+ # load list of seen nodes from disk
|
|
seen_nodes = shelve.open('nodes.seen', writeback=True)
|
|
seen_nodes = shelve.open('nodes.seen', writeback=True)
|
|
bot.memory['seen_nodes'] = seen_nodes
|
|
bot.memory['seen_nodes'] = seen_nodes
|
|
|
|
|
|
|
|
+ # no need to configure anything else if the ffpb config section is missing
|
|
if not bot.config.has_section('ffpb'):
|
|
if not bot.config.has_section('ffpb'):
|
|
bot.memory['ffpb_in_setup'] = False
|
|
bot.memory['ffpb_in_setup'] = False
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # open the git repository containing the peers files
|
|
if not bot.config.ffpb.peers_directory is None:
|
|
if not bot.config.ffpb.peers_directory is None:
|
|
peers_repo = git.Repo(bot.config.ffpb.peers_directory)
|
|
peers_repo = git.Repo(bot.config.ffpb.peers_directory)
|
|
assert peers_repo.bare == False
|
|
assert peers_repo.bare == False
|
|
|
|
|
|
|
|
+ # if configured, start the messaging server
|
|
if int(bot.config.ffpb.msg_enable) == 1:
|
|
if int(bot.config.ffpb.msg_enable) == 1:
|
|
host = "localhost"
|
|
host = "localhost"
|
|
port = 2342
|
|
port = 2342
|
|
@@ -109,29 +121,35 @@ def setup(bot):
|
|
msgserver_thread.daemon = True
|
|
msgserver_thread.daemon = True
|
|
msgserver_thread.start()
|
|
msgserver_thread.start()
|
|
|
|
|
|
|
|
+ # initially fetch ALFRED data
|
|
alfred_method = bot.config.ffpb.alfred_method
|
|
alfred_method = bot.config.ffpb.alfred_method
|
|
ffpb_updatealfred(bot)
|
|
ffpb_updatealfred(bot)
|
|
|
|
|
|
|
|
+ # signal end of setup routine
|
|
bot.memory['ffpb_in_setup'] = False
|
|
bot.memory['ffpb_in_setup'] = False
|
|
|
|
|
|
def shutdown(bot):
|
|
def shutdown(bot):
|
|
global msgserver, highscores, monitored_nodes
|
|
global msgserver, highscores, monitored_nodes
|
|
|
|
|
|
|
|
+ # store highscores
|
|
if not highscores is None:
|
|
if not highscores is None:
|
|
highscores.sync()
|
|
highscores.sync()
|
|
highscores.close()
|
|
highscores.close()
|
|
highscores = None
|
|
highscores = None
|
|
|
|
|
|
|
|
+ # store monitored nodes
|
|
if not monitored_nodes is None:
|
|
if not monitored_nodes is None:
|
|
monitored_nodes.sync()
|
|
monitored_nodes.sync()
|
|
monitored_nodes.close()
|
|
monitored_nodes.close()
|
|
monitored_nodes = None
|
|
monitored_nodes = None
|
|
|
|
|
|
|
|
+ # store seen nodes
|
|
if 'seen_nodes' in bot.memory and bot.memory['seen_nodes'] != None:
|
|
if 'seen_nodes' in bot.memory and bot.memory['seen_nodes'] != None:
|
|
bot.memory['seen_nodes'].close()
|
|
bot.memory['seen_nodes'].close()
|
|
bot.memory['seen_nodes'] = None
|
|
bot.memory['seen_nodes'] = None
|
|
del(bot.memory['seen_nodes'])
|
|
del(bot.memory['seen_nodes'])
|
|
|
|
|
|
|
|
+ # shutdown messaging server
|
|
if not msgserver is None:
|
|
if not msgserver is None:
|
|
msgserver.shutdown()
|
|
msgserver.shutdown()
|
|
print("Closed messaging server.")
|
|
print("Closed messaging server.")
|
|
@@ -141,14 +159,17 @@ def shutdown(bot):
|
|
@willie.module.commands("hilfe")
|
|
@willie.module.commands("hilfe")
|
|
@willie.module.commands("man")
|
|
@willie.module.commands("man")
|
|
def ffpb_help(bot, trigger):
|
|
def ffpb_help(bot, trigger):
|
|
|
|
+ """Display commony ulsed functions."""
|
|
|
|
+
|
|
functions = {
|
|
functions = {
|
|
"!ping <knoten>": "Prüfe ob der Knoten erreichbar ist.",
|
|
"!ping <knoten>": "Prüfe ob der Knoten erreichbar ist.",
|
|
"!status": "Aktuellen Status des Netzwerks (insb. Anzahl Knoten und Clients) ausgegeben.",
|
|
"!status": "Aktuellen Status des Netzwerks (insb. Anzahl Knoten und Clients) ausgegeben.",
|
|
"!info <knoten>": "Allgemeine Information zu dem Knoten anzeigen.",
|
|
"!info <knoten>": "Allgemeine Information zu dem Knoten anzeigen.",
|
|
"!link <knoten>": "MAC-Adresse und Link zur Status-Seite des Knotens 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)",
|
|
"!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)
|
|
param = trigger.group(2)
|
|
if param is None:
|
|
if param is None:
|
|
bot.say("Funktionen: " + str.join(", ", sorted(functions.keys())))
|
|
bot.say("Funktionen: " + str.join(", ", sorted(functions.keys())))
|
|
@@ -164,6 +185,9 @@ def ffpb_help(bot, trigger):
|
|
bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
|
|
bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
|
|
|
|
|
|
def ffpb_findnode(name):
|
|
def ffpb_findnode(name):
|
|
|
|
+ """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:
|
|
if name is None or len(name) == 0:
|
|
return None
|
|
return None
|
|
|
|
|
|
@@ -178,7 +202,7 @@ def ffpb_findnode(name):
|
|
if mac in alfred_data:
|
|
if mac in alfred_data:
|
|
return alfred_data[mac]
|
|
return alfred_data[mac]
|
|
|
|
|
|
- # try to find alias MAC
|
|
|
|
|
|
+ # try to find alias MAC in ALFRED data
|
|
for nodeid in alfred_data:
|
|
for nodeid in alfred_data:
|
|
node = alfred_data[nodeid]
|
|
node = alfred_data[nodeid]
|
|
if "network" in node:
|
|
if "network" in node:
|
|
@@ -205,7 +229,7 @@ def ffpb_findnode(name):
|
|
else:
|
|
else:
|
|
names[h] = nodeid
|
|
names[h] = nodeid
|
|
|
|
|
|
- # still not found -> try peers_repo
|
|
|
|
|
|
+ # not found in ALFRED data -> try peers_repo
|
|
if not peers_repo is None:
|
|
if not peers_repo is None:
|
|
peer_name = None
|
|
peer_name = None
|
|
peer_mac = None
|
|
peer_mac = None
|
|
@@ -238,9 +262,12 @@ def ffpb_findnode(name):
|
|
# if we got exactly one candidate that might be it
|
|
# if we got exactly one candidate that might be it
|
|
return alfred_data[names[possibilities[0]]]
|
|
return alfred_data[names[possibilities[0]]]
|
|
|
|
|
|
|
|
+ # none of the above was able to identify the requested node
|
|
return None
|
|
return None
|
|
|
|
|
|
def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata = True):
|
|
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 (name is None or len(name) == 0):
|
|
if not bot is None: bot.reply("Grün.")
|
|
if not bot is None: bot.reply("Grün.")
|
|
return None
|
|
return None
|
|
@@ -256,10 +283,13 @@ def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata = True):
|
|
node = ffpb_findnode(name)
|
|
node = ffpb_findnode(name)
|
|
if node is None:
|
|
if node is None:
|
|
if not bot is None: bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
|
|
if not bot is None: bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
|
|
-
|
|
|
|
|
|
+
|
|
return node
|
|
return node
|
|
|
|
|
|
def mac2ipv6(mac, prefix=None):
|
|
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())
|
|
result = str(netaddr.EUI(mac).ipv6_link_local())
|
|
if (not prefix is None) and (result.startswith("fe80::")):
|
|
if (not prefix is None) and (result.startswith("fe80::")):
|
|
result = prefix + result[6:]
|
|
result = prefix + result[6:]
|
|
@@ -332,7 +362,6 @@ def ffpb_updatealfred(bot):
|
|
action_target = bot.config.ffpb.msg_target
|
|
action_target = bot.config.ffpb.msg_target
|
|
bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
|
|
bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
|
|
|
|
|
|
-
|
|
|
|
def ffpb_alfred_data_outdated():
|
|
def ffpb_alfred_data_outdated():
|
|
timeout = datetime.datetime.now() - datetime.timedelta(minutes=5)
|
|
timeout = datetime.datetime.now() - datetime.timedelta(minutes=5)
|
|
is_outdated = timeout > alfred_update
|
|
is_outdated = timeout > alfred_update
|
|
@@ -341,6 +370,8 @@ def ffpb_alfred_data_outdated():
|
|
|
|
|
|
@willie.module.commands('debug-alfred')
|
|
@willie.module.commands('debug-alfred')
|
|
def ffpb_debug_alfred(bot, trigger):
|
|
def ffpb_debug_alfred(bot, trigger):
|
|
|
|
+ """Show statistics of available ALFRED data."""
|
|
|
|
+
|
|
if alfred_data is None:
|
|
if alfred_data is None:
|
|
bot.say("Keine ALFRED-Daten vorhanden.")
|
|
bot.say("Keine ALFRED-Daten vorhanden.")
|
|
else:
|
|
else:
|
|
@@ -348,28 +379,38 @@ def ffpb_debug_alfred(bot, trigger):
|
|
|
|
|
|
@willie.module.commands('alfred-data')
|
|
@willie.module.commands('alfred-data')
|
|
def ffpb_peerdata(bot, trigger):
|
|
def ffpb_peerdata(bot, trigger):
|
|
|
|
+ """Show ALFRED data of the given node."""
|
|
|
|
+
|
|
|
|
+ # user must be a bot admin
|
|
if (not trigger.admin):
|
|
if (not trigger.admin):
|
|
bot.say('I wont leak (possibly) sensitive data to you.')
|
|
bot.say('I wont leak (possibly) sensitive data to you.')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # query must be a PM or as OP in the channel
|
|
if (not trigger.is_privmsg) and (not trigger.nick in bot.ops[trigger.sender]):
|
|
if (not trigger.is_privmsg) and (not trigger.nick in bot.ops[trigger.sender]):
|
|
bot.say('Kein Keks? Keine Daten.')
|
|
bot.say('Kein Keks? Keine Daten.')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # identify node or bail out
|
|
target_name = trigger.group(2)
|
|
target_name = trigger.group(2)
|
|
node = ffpb_findnode_from_botparam(bot, target_name)
|
|
node = ffpb_findnode_from_botparam(bot, target_name)
|
|
if node is None: return
|
|
if node is None: return
|
|
|
|
|
|
|
|
+ # reply each key in the node's data
|
|
for key in node:
|
|
for key in node:
|
|
if key in [ 'hostname' ]: continue
|
|
if key in [ 'hostname' ]: continue
|
|
bot.say("{0}.{1} = {2}".format(node['hostname'], key, str(node[key])))
|
|
bot.say("{0}.{1} = {2}".format(node['hostname'], key, str(node[key])))
|
|
|
|
|
|
@willie.module.commands('info')
|
|
@willie.module.commands('info')
|
|
def ffpb_peerinfo(bot, trigger):
|
|
def ffpb_peerinfo(bot, trigger):
|
|
|
|
+ """Show information of the given node."""
|
|
|
|
+
|
|
|
|
+ # identify node or bail out
|
|
target_name = trigger.group(2)
|
|
target_name = trigger.group(2)
|
|
node = ffpb_findnode_from_botparam(bot, target_name)
|
|
node = ffpb_findnode_from_botparam(bot, target_name)
|
|
if node is None: return
|
|
if node is None: return
|
|
|
|
|
|
|
|
+ # read node information
|
|
info_mac = node["network"]["mac"]
|
|
info_mac = node["network"]["mac"]
|
|
info_name = node["hostname"]
|
|
info_name = node["hostname"]
|
|
|
|
|
|
@@ -412,10 +453,14 @@ def ffpb_peerinfo(bot, trigger):
|
|
|
|
|
|
@willie.module.commands('uptime')
|
|
@willie.module.commands('uptime')
|
|
def ffpb_peeruptime(bot, trigger):
|
|
def ffpb_peeruptime(bot, trigger):
|
|
|
|
+ """Display the uptime of the given node."""
|
|
|
|
+
|
|
|
|
+ # identify node or bail out
|
|
target_name = trigger.group(2)
|
|
target_name = trigger.group(2)
|
|
node = ffpb_findnode_from_botparam(bot, target_name)
|
|
node = ffpb_findnode_from_botparam(bot, target_name)
|
|
if node is None: return
|
|
if node is None: return
|
|
|
|
|
|
|
|
+ # get name and raw uptime from node
|
|
info_name = node["hostname"]
|
|
info_name = node["hostname"]
|
|
info_uptime = ''
|
|
info_uptime = ''
|
|
u_raw = None
|
|
u_raw = None
|
|
@@ -424,6 +469,7 @@ def ffpb_peeruptime(bot, trigger):
|
|
elif 'uptime' in node:
|
|
elif 'uptime' in node:
|
|
u_raw = node['uptime']
|
|
u_raw = node['uptime']
|
|
|
|
|
|
|
|
+ # pretty print uptime
|
|
if not u_raw is None:
|
|
if not u_raw is None:
|
|
u = int(float(u_raw))
|
|
u = int(float(u_raw))
|
|
d, r1 = divmod(u, 86400)
|
|
d, r1 = divmod(u, 86400)
|
|
@@ -438,22 +484,32 @@ def ffpb_peeruptime(bot, trigger):
|
|
else:
|
|
else:
|
|
info_uptime += '?'
|
|
info_uptime += '?'
|
|
|
|
|
|
|
|
+ # reply to user
|
|
bot.say('uptime(\'{0}\') = {1}'.format(info_name, info_uptime))
|
|
bot.say('uptime(\'{0}\') = {1}'.format(info_name, info_uptime))
|
|
|
|
|
|
@willie.module.commands('link')
|
|
@willie.module.commands('link')
|
|
def ffpb_peerlink(bot, trigger):
|
|
def ffpb_peerlink(bot, trigger):
|
|
|
|
+ """Display MAC and link to statuspage for the given node."""
|
|
|
|
+
|
|
|
|
+ # identify node or bail out
|
|
target_name = trigger.group(2)
|
|
target_name = trigger.group(2)
|
|
node = ffpb_findnode_from_botparam(bot, target_name)
|
|
node = ffpb_findnode_from_botparam(bot, target_name)
|
|
if node is None: return
|
|
if node is None: return
|
|
|
|
|
|
|
|
+ # get node's MAC
|
|
info_mac = node["network"]["mac"]
|
|
info_mac = node["network"]["mac"]
|
|
info_name = node["hostname"]
|
|
info_name = node["hostname"]
|
|
|
|
+
|
|
|
|
+ # get node's v6 address in the mesh (derived from MAC address)
|
|
info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:')
|
|
info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:')
|
|
|
|
+
|
|
|
|
+ # reply to user
|
|
bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6))
|
|
bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6))
|
|
|
|
|
|
@willie.module.interval(60)
|
|
@willie.module.interval(60)
|
|
def ffpb_updatepeers(bot):
|
|
def ffpb_updatepeers(bot):
|
|
- """Aktualisiere die Knotenliste und melde das Diff"""
|
|
|
|
|
|
+ """Refresh list of peers and message the diff."""
|
|
|
|
+
|
|
if peers_repo is None:
|
|
if peers_repo is None:
|
|
print('WARNING: peers_repo is None')
|
|
print('WARNING: peers_repo is None')
|
|
return
|
|
return
|
|
@@ -495,6 +551,9 @@ def ffpb_updatepeers(bot):
|
|
bot.msg(bot.config.ffpb.msg_target, response)
|
|
bot.msg(bot.config.ffpb.msg_target, response)
|
|
|
|
|
|
def ffpb_fetch_stats(bot, url, memoryid):
|
|
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)
|
|
response = urllib2.urlopen(url)
|
|
|
|
|
|
data = json.load(response)
|
|
data = json.load(response)
|
|
@@ -529,8 +588,10 @@ def ffpb_fetch_stats(bot, url, memoryid):
|
|
|
|
|
|
@willie.module.interval(15)
|
|
@willie.module.interval(15)
|
|
def ffpb_get_stats(bot):
|
|
def ffpb_get_stats(bot):
|
|
|
|
+ """Fetch current statistics, if the highscore changes signal this."""
|
|
|
|
+
|
|
(nodes_active, nodes_total, clients_count) = ffpb_fetch_stats(bot, 'http://map.paderborn.freifunk.net/nodes.json', 'ffpb_stats')
|
|
(nodes_active, nodes_total, clients_count) = ffpb_fetch_stats(bot, 'http://map.paderborn.freifunk.net/nodes.json', 'ffpb_stats')
|
|
-
|
|
|
|
|
|
+
|
|
highscore_changed = False
|
|
highscore_changed = False
|
|
if nodes_active > highscores['nodes']:
|
|
if nodes_active > highscores['nodes']:
|
|
highscores['nodes'] = nodes_active
|
|
highscores['nodes'] = nodes_active
|
|
@@ -553,7 +614,8 @@ def ffpb_get_stats(bot):
|
|
|
|
|
|
@willie.module.commands('status')
|
|
@willie.module.commands('status')
|
|
def ffpb_status(bot, trigger):
|
|
def ffpb_status(bot, trigger):
|
|
- """Status des FFPB-Netzes: Anzahl (aktiver) Knoten + Clients"""
|
|
|
|
|
|
+ """State of the network: count of nodes + clients"""
|
|
|
|
+
|
|
stats = bot.memory['ffpb_stats'] if 'ffpb_stats' in bot.memory else None
|
|
stats = bot.memory['ffpb_stats'] if 'ffpb_stats' in bot.memory else None
|
|
if stats is None:
|
|
if stats is None:
|
|
bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?')
|
|
bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?')
|
|
@@ -612,20 +674,26 @@ def ffpb_highscore(bot, trigger):
|
|
|
|
|
|
@willie.module.commands('rollout-status')
|
|
@willie.module.commands('rollout-status')
|
|
def ffpb_rolloutstatus(bot, trigger):
|
|
def ffpb_rolloutstatus(bot, trigger):
|
|
|
|
+ """Display statistic on how many nodes have installed the given firmware version."""
|
|
|
|
+
|
|
|
|
+ # initialize results dictionary
|
|
result = { }
|
|
result = { }
|
|
for branch in [ 'stable', 'testing' ]:
|
|
for branch in [ 'stable', 'testing' ]:
|
|
result[branch] = None
|
|
result[branch] = None
|
|
skipped = 0
|
|
skipped = 0
|
|
|
|
|
|
|
|
+ # command is restricted to bot-admins via PM or OPS in the channel
|
|
if (not (trigger.admin and trigger.is_privmsg)) and (not trigger.nick in bot.ops[trigger.sender]):
|
|
if (not (trigger.admin and trigger.is_privmsg)) and (not trigger.nick in bot.ops[trigger.sender]):
|
|
bot.say('Geh zur dunklen Seite, die haben Kekse - ohne Keks kein Rollout-Status.')
|
|
bot.say('Geh zur dunklen Seite, die haben Kekse - ohne Keks kein Rollout-Status.')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # read expected firmware version from command arguments
|
|
expected_release = trigger.group(2)
|
|
expected_release = trigger.group(2)
|
|
if expected_release is None or len(expected_release) == 0:
|
|
if expected_release is None or len(expected_release) == 0:
|
|
bot.say('Von welcher Firmware denn?')
|
|
bot.say('Von welcher Firmware denn?')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # check each node in ALFRED data
|
|
for nodeid in alfred_data:
|
|
for nodeid in alfred_data:
|
|
item = alfred_data[nodeid]
|
|
item = alfred_data[nodeid]
|
|
if (not 'software' in item) or (not 'firmware' in item['software']) or (not 'autoupdater' in item['software']):
|
|
if (not 'software' in item) or (not 'firmware' in item['software']) or (not 'autoupdater' in item['software']):
|
|
@@ -643,6 +711,7 @@ def ffpb_rolloutstatus(bot, trigger):
|
|
mode = 'auto' if enabled else 'manual'
|
|
mode = 'auto' if enabled else 'manual'
|
|
result[branch][mode+'_'+match] += 1
|
|
result[branch][mode+'_'+match] += 1
|
|
|
|
|
|
|
|
+ # respond to user
|
|
output = "Rollout von '{0}':".format(expected_release)
|
|
output = "Rollout von '{0}':".format(expected_release)
|
|
for branch in result:
|
|
for branch in result:
|
|
auto_count = result[branch]['auto_count']
|
|
auto_count = result[branch]['auto_count']
|
|
@@ -650,19 +719,26 @@ def ffpb_rolloutstatus(bot, trigger):
|
|
manual_count = result[branch]['manual_count']
|
|
manual_count = result[branch]['manual_count']
|
|
manual_total = manual_count + result[branch]['manual_not']
|
|
manual_total = manual_count + result[branch]['manual_not']
|
|
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))
|
|
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))
|
|
|
|
+
|
|
|
|
+ # output count of nodes for which the autoupdater's branch and/or
|
|
|
|
+ # firmware version could not be retrieved
|
|
if skipped > 0:
|
|
if skipped > 0:
|
|
bot.say("Rollout von '{0}': {1} Knoten unklar".format(expected_release, skipped))
|
|
bot.say("Rollout von '{0}': {1} Knoten unklar".format(expected_release, skipped))
|
|
|
|
|
|
@willie.module.commands('ping')
|
|
@willie.module.commands('ping')
|
|
def ffpb_ping(bot, trigger=None, target_name=None):
|
|
def ffpb_ping(bot, trigger=None, target_name=None):
|
|
- """Ping FFPB-Knoten"""
|
|
|
|
|
|
+ """Ping the given node"""
|
|
|
|
+
|
|
|
|
+ # identify node or bail out
|
|
if target_name is None: target_name = trigger.group(2)
|
|
if target_name is None: target_name = trigger.group(2)
|
|
node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
|
|
node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
|
|
if node is None: return None
|
|
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 = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
|
|
target_alias = node["hostname"]
|
|
target_alias = node["hostname"]
|
|
|
|
|
|
|
|
+ # execute the actual ping and reply the result
|
|
print("pinging '{0}' at {1} ...".format(target_name, target))
|
|
print("pinging '{0}' at {1} ...".format(target_name, target))
|
|
result = os.system('ping6 -c 2 -W 1 ' + target + ' >/dev/null')
|
|
result = os.system('ping6 -c 2 -W 1 ' + target + ' >/dev/null')
|
|
if result == 0:
|
|
if result == 0:
|
|
@@ -680,10 +756,14 @@ def ffpb_ping(bot, trigger=None, target_name=None):
|
|
|
|
|
|
@willie.module.interval(3*60)
|
|
@willie.module.interval(3*60)
|
|
def ffpb_monitor_ping(bot):
|
|
def ffpb_monitor_ping(bot):
|
|
|
|
+ """Ping each node currently under surveillance."""
|
|
|
|
+
|
|
|
|
+ # determine where-to to send alerts
|
|
notify_target = bot.config.core.owner
|
|
notify_target = bot.config.core.owner
|
|
if (not bot.config.ffpb.msg_target is None):
|
|
if (not bot.config.ffpb.msg_target is None):
|
|
notify_target = bot.config.ffpb.msg_target
|
|
notify_target = bot.config.ffpb.msg_target
|
|
|
|
|
|
|
|
+ # check each node under surveillance
|
|
for node in monitored_nodes:
|
|
for node in monitored_nodes:
|
|
mon = monitored_nodes[node]
|
|
mon = monitored_nodes[node]
|
|
added = mon['added']
|
|
added = mon['added']
|
|
@@ -709,18 +789,24 @@ def ffpb_monitor_ping(bot):
|
|
|
|
|
|
@willie.module.commands('monitor')
|
|
@willie.module.commands('monitor')
|
|
def ffpb_monitor(bot, trigger):
|
|
def ffpb_monitor(bot, trigger):
|
|
|
|
+ """Monitoring capability of the bot, try subcommands add, del, info and list."""
|
|
|
|
+
|
|
|
|
+ # command is restricted to bot admins
|
|
if not trigger.admin:
|
|
if not trigger.admin:
|
|
bot.say('Ich ping hier nicht für jeden durch die Weltgeschichte.')
|
|
bot.say('Ich ping hier nicht für jeden durch die Weltgeschichte.')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # ensure the user gave arguments (group 2 is the concatenation of all following groups)
|
|
if trigger.group(2) is None or len(trigger.group(2)) == 0:
|
|
if trigger.group(2) is None or len(trigger.group(2)) == 0:
|
|
bot.say('Das Monitoring sagt du hast doofe Ohren.')
|
|
bot.say('Das Monitoring sagt du hast doofe Ohren.')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # read additional arguments
|
|
cmd = trigger.group(3)
|
|
cmd = trigger.group(3)
|
|
node = trigger.group(4)
|
|
node = trigger.group(4)
|
|
if not node is None: node = str(node)
|
|
if not node is None: node = str(node)
|
|
|
|
|
|
|
|
+ # subcommand 'add': add a node to monitoring
|
|
if cmd == "add":
|
|
if cmd == "add":
|
|
if node in monitored_nodes:
|
|
if node in monitored_nodes:
|
|
bot.say('Knoten \'{0}\' wird bereits gemonitored.'.format(node))
|
|
bot.say('Knoten \'{0}\' wird bereits gemonitored.'.format(node))
|
|
@@ -736,6 +822,7 @@ def ffpb_monitor(bot, trigger):
|
|
bot.say('Knoten \'{0}\' wird jetzt ganz genau beobachtet.'.format(node))
|
|
bot.say('Knoten \'{0}\' wird jetzt ganz genau beobachtet.'.format(node))
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # subcommand 'del': remote a node from monitoring
|
|
if cmd == "del":
|
|
if cmd == "del":
|
|
if not node in monitored_nodes:
|
|
if not node in monitored_nodes:
|
|
bot.say('Knoten \'{0}\' war gar nicht im Monitoring?!?'.format(node))
|
|
bot.say('Knoten \'{0}\' war gar nicht im Monitoring?!?'.format(node))
|
|
@@ -744,6 +831,7 @@ def ffpb_monitor(bot, trigger):
|
|
bot.say('Okidoki, \'{0}\' lasse ich jetzt links liegen.'.format(node))
|
|
bot.say('Okidoki, \'{0}\' lasse ich jetzt links liegen.'.format(node))
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # subcommand 'info': monitoring status of a node
|
|
if cmd == "info":
|
|
if cmd == "info":
|
|
if node in monitored_nodes:
|
|
if node in monitored_nodes:
|
|
info = monitored_nodes[node]
|
|
info = monitored_nodes[node]
|
|
@@ -752,6 +840,7 @@ def ffpb_monitor(bot, trigger):
|
|
bot.say('Knoten \'{0}\' ist nicht im Monitoring.'.format(node))
|
|
bot.say('Knoten \'{0}\' ist nicht im Monitoring.'.format(node))
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # subcommand 'list': enumerate all monitored nodes
|
|
if cmd == "list":
|
|
if cmd == "list":
|
|
nodes = ""
|
|
nodes = ""
|
|
for node in monitored_nodes:
|
|
for node in monitored_nodes:
|
|
@@ -759,40 +848,53 @@ def ffpb_monitor(bot, trigger):
|
|
bot.say('Monitoring aktiv für:' + nodes)
|
|
bot.say('Monitoring aktiv für:' + nodes)
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # subcommand 'help': give some hints what the user can do
|
|
if cmd == "help":
|
|
if cmd == "help":
|
|
bot.say('Entweder "!monitor list" oder "!monitor {add|del|info} <node>"')
|
|
bot.say('Entweder "!monitor list" oder "!monitor {add|del|info} <node>"')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # no valid subcommand given: complain
|
|
bot.say('Mit "' + str(cmd) + '" kann ich nix anfangen, probier doch mal "!monitor help".')
|
|
bot.say('Mit "' + str(cmd) + '" kann ich nix anfangen, probier doch mal "!monitor help".')
|
|
|
|
|
|
@willie.module.commands('providers')
|
|
@willie.module.commands('providers')
|
|
def ffpb_providers(bot, trigger):
|
|
def ffpb_providers(bot, trigger):
|
|
|
|
+ """Fetch the top 5 providers from BATCAVE."""
|
|
|
|
+
|
|
providers = json.load(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/providers?format=json'))
|
|
providers = json.load(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/providers?format=json'))
|
|
providers.sort(key=lambda x: x['count'], reverse=True)
|
|
providers.sort(key=lambda x: x['count'], reverse=True)
|
|
bot.say('Unsere Top 5 Provider: ' + ', '.join(['{0} ({1:.0f}%)'.format(x['name'], x['percentage']) for x in providers[:5]]))
|
|
bot.say('Unsere Top 5 Provider: ' + ', '.join(['{0} ({1:.0f}%)'.format(x['name'], x['percentage']) for x in providers[:5]]))
|
|
|
|
|
|
@willie.module.commands('mesh')
|
|
@willie.module.commands('mesh')
|
|
def ffpb_nodemesh(bot, trigger):
|
|
def ffpb_nodemesh(bot, trigger):
|
|
|
|
+ """Display mesh partners of the given node."""
|
|
|
|
+
|
|
|
|
+ # identify node or bail out
|
|
target_name = trigger.group(2)
|
|
target_name = trigger.group(2)
|
|
node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
|
|
node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
|
|
if node is None: return None
|
|
if node is None: return None
|
|
|
|
|
|
|
|
+ # derive node's id
|
|
nodeid = node['node_id'] if 'node_id' in node else None
|
|
nodeid = node['node_id'] if 'node_id' in node else None
|
|
if nodeid is None: nodeid = node['network']['mac'].replace(':','') if 'network' in node and 'mac' in node['network'] else None
|
|
if nodeid is None: nodeid = node['network']['mac'].replace(':','') if 'network' in node and 'mac' in node['network'] else None
|
|
if nodeid is None:
|
|
if nodeid is None:
|
|
bot.say('Mist, ich habe gerade den Zettel verlegt auf dem die Node-ID von \'{0}\' steht, bitte frag später noch einmal.'.format(node['hostname'] if 'hostname' in node else target_name))
|
|
bot.say('Mist, ich habe gerade den Zettel verlegt auf dem die Node-ID von \'{0}\' steht, bitte frag später noch einmal.'.format(node['hostname'] if 'hostname' in node else target_name))
|
|
return
|
|
return
|
|
|
|
+
|
|
|
|
+ # query BATCAVE for node's neighbours (result is a list of MAC addresses)
|
|
cave_result = json.load(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/node/{0}/neighbours'.format(nodeid)))
|
|
cave_result = json.load(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/node/{0}/neighbours'.format(nodeid)))
|
|
|
|
|
|
|
|
+ # query BATCAVE for neighbour's names
|
|
d = '&'.join([ str(n) for n in cave_result ])
|
|
d = '&'.join([ str(n) for n in cave_result ])
|
|
req = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/idmac2name', d)
|
|
req = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/idmac2name', d)
|
|
|
|
|
|
|
|
+ # filter out duplicate names
|
|
neighbours = set()
|
|
neighbours = set()
|
|
for n in req:
|
|
for n in req:
|
|
ident,name = n.strip().split('=')
|
|
ident,name = n.strip().split('=')
|
|
neighbours.add(name)
|
|
neighbours.add(name)
|
|
neighbours = [ x for x in neighbours ]
|
|
neighbours = [ x for x in neighbours ]
|
|
|
|
|
|
|
|
+ # respond to the user
|
|
if len(neighbours) == 0:
|
|
if len(neighbours) == 0:
|
|
bot.say(u'{0} hat keinen Mesh-Partner *schnüff*'.format(node['hostname']))
|
|
bot.say(u'{0} hat keinen Mesh-Partner *schnüff*'.format(node['hostname']))
|
|
elif len(neighbours) == 1:
|
|
elif len(neighbours) == 1:
|
|
@@ -802,7 +904,8 @@ def ffpb_nodemesh(bot, trigger):
|
|
|
|
|
|
@willie.module.commands('exec-on-peer')
|
|
@willie.module.commands('exec-on-peer')
|
|
def ffpb_remoteexec(bot, trigger):
|
|
def ffpb_remoteexec(bot, trigger):
|
|
- """Remote Execution fuer FFPB_Knoten"""
|
|
|
|
|
|
+ """Remote execution on the given node"""
|
|
|
|
+
|
|
bot_params = trigger.group(2).split(' ',1)
|
|
bot_params = trigger.group(2).split(' ',1)
|
|
if len(bot_params) != 2:
|
|
if len(bot_params) != 2:
|
|
bot.say('Wenn du nicht sagst wo mach ich remote execution bei dir!')
|
|
bot.say('Wenn du nicht sagst wo mach ich remote execution bei dir!')
|
|
@@ -812,28 +915,37 @@ def ffpb_remoteexec(bot, trigger):
|
|
target_name = bot_params[0]
|
|
target_name = bot_params[0]
|
|
target_cmd = bot_params[1]
|
|
target_cmd = bot_params[1]
|
|
|
|
|
|
|
|
+ # remote execution may only be triggered by bot admins
|
|
if not trigger.admin:
|
|
if not trigger.admin:
|
|
bot.say('I can haz sudo?')
|
|
bot.say('I can haz sudo?')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # make sure remote execution is done in public
|
|
if trigger.is_privmsg:
|
|
if trigger.is_privmsg:
|
|
bot.say('Bitte per Channel.')
|
|
bot.say('Bitte per Channel.')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # double-safety: user must be op in the channel, too (hoping for NickServ authentication)
|
|
if not trigger.nick in bot.ops[trigger.sender]:
|
|
if not trigger.nick in bot.ops[trigger.sender]:
|
|
bot.say('Geh weg.')
|
|
bot.say('Geh weg.')
|
|
return
|
|
return
|
|
|
|
|
|
|
|
+ # identify requested node or bail out
|
|
node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
|
|
node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
|
|
if node is None: return
|
|
if node is None: return
|
|
|
|
|
|
|
|
+ # use the node's first non-linklocal address
|
|
target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
|
|
target = [x for x in node["network"]["addresses"] if not x.lower().startswith("fe80:")][0]
|
|
target_alias = node["hostname"]
|
|
target_alias = node["hostname"]
|
|
|
|
|
|
|
|
+ # assemble SSH command
|
|
cmd = 'ssh -6 -l root ' + target + ' -- "' + target_cmd + '"'
|
|
cmd = 'ssh -6 -l root ' + target + ' -- "' + target_cmd + '"'
|
|
print("REMOTE EXEC = " + cmd)
|
|
print("REMOTE EXEC = " + cmd)
|
|
try:
|
|
try:
|
|
|
|
+ # call SSH
|
|
result = subprocess.check_output(['ssh', '-6n', '-l', 'root', '-o', 'BatchMode=yes', '-o','StrictHostKeyChecking=no', target, target_cmd], stderr=subprocess.STDOUT, shell=False)
|
|
result = subprocess.check_output(['ssh', '-6n', '-l', 'root', '-o', 'BatchMode=yes', '-o','StrictHostKeyChecking=no', target, target_cmd], stderr=subprocess.STDOUT, shell=False)
|
|
|
|
+
|
|
|
|
+ # fetch results and sent at most 8 of them as response
|
|
lines = str(result).splitlines()
|
|
lines = str(result).splitlines()
|
|
|
|
|
|
if len(lines) == 0:
|
|
if len(lines) == 0:
|