Kaynağa Gözat

split ffpb into ffpb_{monitoring,nodeinfos,status}

Helge Jung 9 yıl önce
ebeveyn
işleme
3d9ba3c29c
4 değiştirilmiş dosya ile 327 ekleme ve 277 silme
  1. 68 277
      modules/ffpb.py
  2. 94 0
      modules/ffpb_monitoring.py
  3. 94 0
      modules/ffpb_nodeinfos.py
  4. 71 0
      modules/ffpb_status.py

+ 68 - 277
modules/ffpb.py

@@ -21,12 +21,8 @@ import threading
 
 msgserver = None
 peers_repo = None
-monitored_nodes = None
-highscores = None
 
 alfred_method = None
-alfred_data = None
-alfred_update = datetime.datetime(1970,1,1,23,42)
 
 ffpb_resolver = dns.resolver.Resolver ()
 ffpb_resolver.nameservers = ['10.132.254.53']
@@ -67,8 +63,11 @@ class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
 	pass
 
 def setup(bot):
-	global msgserver, peers_repo, alfred_method, highscores, monitored_nodes
+	global msgserver, peers_repo, alfred_method
 
+	# open highscores file (backed to filesystem)
+	if 'highscores' in bot.memory and not bot.memory['highscores'] is None:
+		bot.memory['highscores'].close()
 	highscores = shelve.open('highscoredata', writeback=True)
 	if not 'nodes' in highscores:
 		highscores['nodes'] = 0
@@ -76,8 +75,7 @@ def setup(bot):
 	if not 'clients' in highscores:
 		highscores['clients'] = 0
 		highscores['clients_ts'] = time.time()
-
-	monitored_nodes = shelve.open('monitorednodes', writeback=True)
+	bot.memory['highscores'] = highscores
 
 	if not bot.config.has_section('ffpb'):
 		return
@@ -105,17 +103,12 @@ def setup(bot):
 	ffpb_updatealfred(bot)
 
 def shutdown(bot):
-	global msgserver, highscores, monitored_nodes
-
-	if not highscores is None:
-		highscores.sync()
-		highscores.close()
-		highscores = None
+	global msgserver
 
-	if not monitored_nodes is None:
-		monitored_nodes.sync()
-		monitored_nodes.close()
-		monitored_nodes = None
+	if 'highscores' in bot.memory and not bot.memory['highscores'] is None:
+		bot.memory['highscores'].sync()
+		bot.memory['highscores'].close()
+		del(bot.memory['highscores'])
 
 	if not msgserver is None:
 		msgserver.shutdown()
@@ -148,36 +141,39 @@ def ffpb_help(bot, trigger):
 
 	bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
 
-def ffpb_findnode(name):
+def ffpb_findnode(bot, name):
 	if name is None or len(name) == 0:
 		return None
 
 	name = str(name).strip()
 
-	# 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 alfred_data[mac]
-
-		# try to find alias MAC
+	alfred_data = bot.memory['alfred_data'] if 'alfred_data' in bot.memory else None
+
+	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 alfred_data[mac]
+
+			# try to find alias MAC
+			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 node
+					if "mesh_interfaces" in node["network"]:
+						for mim in node["network"]["mesh_interfaces"]:
+							if mim.lower() == mac:
+								return node
+
+		# look through the ALFRED peers
+		possible_matches = []
 		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 node
-				if "mesh_interfaces" in node["network"]:
-					for mim in node["network"]["mesh_interfaces"]:
-						if mim.lower() == mac:
-							return node
-
-	# look through the ALFRED peers
-	possible_matches = []
-	for nodeid in alfred_data:
-		node = alfred_data[nodeid]
-		if "hostname" in node and node["hostname"].lower() == name.lower():
-			return node
+			if "hostname" in node and node["hostname"].lower() == name.lower():
+				return node
 
 	# still not found -> try peers_repo
 	if not peers_repo is None:
@@ -203,20 +199,33 @@ def ffpb_findnode(name):
 
 	return None
 
+def ffpb_get_alfreddata(bot, ensure_recent=True):
+	if not 'alfred_data' in bot.memory or bot.memory['alfred_data'] is None:
+		return None
+
+	if ensure_recent:
+		alfred_update = bot.memory['alfred_update'] if 'alfred_update' in bot.memory else None
+		if alfred_update is None: return None
+
+		timeout = datetime.datetime.now() - datetime.timedelta(minutes=5)
+		is_outdated = timeout > alfred_update
+		#print("ALFRED outdated? {0} (timeout={1} vs. lastupdate={2})".format(is_outdated, timeout, alfred_update))
+		if is_outdated:
+			return None
+
+	return bot.memory['alfred_data']
+
 def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata = True):
 	if (name is None or len(name) == 0):
 		bot.reply("Grün.")
 		return None
 
-	if ensure_recent_alfreddata and alfred_data is None:
-		bot.say("Informationen sind ausverkauft, kommen erst morgen wieder rein.")
-		return None
-
-	if ensure_recent_alfreddata and ffpb_alfred_data_outdated():
-		bot.say("Ich habe gerade keine aktuellen Informationen, daher sage ich mal lieber nichts zu '" + name + "'.")
+	alfred_data = ffpb_get_alfreddata(bot, ensure_recent_alfreddata)
+	if alfred_data is None:
+		bot.say("Ich habe gerade keine (aktuellen) Informationen, daher sage ich mal lieber nichts zu '" + name + "'.")
 		return None
 
-	node = ffpb_findnode(name)
+	node = ffpb_findnode(bot, name)
 	if node is None:
 		bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
 		
@@ -231,11 +240,11 @@ def mac2ipv6(mac, prefix=None):
 @willie.module.interval(30)
 def ffpb_updatealfred(bot):
 	"""Aktualisiere ALFRED-Daten"""
-	global alfred_data, alfred_update
 
 	if alfred_method is None or alfred_method == "None":
 		return
 
+	alfred_data = None
 	updated = None
 	if alfred_method == "exec":
 		rawdata = subprocess.check_output(['alfred-json', '-z', '-r', '158'])
@@ -249,30 +258,28 @@ def ffpb_updatealfred(bot):
 		updated = datetime.datetime.fromtimestamp(mktime_tz(rawdata.info().getdate_tz("Last-Modified")))
 	else:
 		print("Unknown ALFRED data method '", alfred_method, "', cannot load new data.", sep="")
-		alfred_data = None
+		bot.memory['alfred_data'] = None
 		return
 
 	try:
 		alfred_data = json.load(rawdata)
 		#print("Fetched new ALFRED data:", len(alfred_data), "entries")
-		alfred_update = updated
 
 	except ValueError as e:
 		print("Failed to parse ALFRED data: " + str(e))
 		return
 
-def ffpb_alfred_data_outdated():
-	timeout = datetime.datetime.now() - datetime.timedelta(minutes=5)
-	is_outdated = timeout > alfred_update
-	#print("ALFRED outdated? {0} (timeout={1} vs. lastupdate={2})".format(is_outdated, timeout, alfred_update))
-	return is_outdated
+	bot.memory['alfred_data'] = alfred_data
+	bot.memory['alfred_update'] = updated
 
 @willie.module.commands('debug-alfred')
 def ffpb_debug_alfred(bot, trigger):
+	alfred_data = ffpb_get_alfreddata(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), alfred_update))
+		bot.say("ALFRED Daten: count={0} lastupdate={1}".format(len(alfred_data), bot.memory['alfred_update'] if 'alfred_memory' in bot.memory else '?'))
 
 @willie.module.commands('alfred-data')
 def ffpb_peerdata(bot, trigger):
@@ -292,93 +299,6 @@ def ffpb_peerdata(bot, trigger):
 		if key in [ 'hostname' ]: continue
 		bot.say("{0}.{1} = {2}".format(node['hostname'], key, str(node[key])))
 
-@willie.module.commands('info')
-def ffpb_peerinfo(bot, trigger):
-	target_name = trigger.group(2)
-	node = ffpb_findnode_from_botparam(bot, target_name)
-	if node is None: return
-
-	info_mac = node["network"]["mac"]
-	info_name = node["hostname"]
-
-	info_hw = ""
-	if "hardware" in node:
-		if "model" in node["hardware"]:
-			model = node["hardware"]["model"]
-			info_hw = " model='" + model + "'"
-
-	info_fw = ""
-	info_update = ""
-	if "software" in node:
-		if "firmware" in node["software"]:
-			fwinfo = str(node["software"]["firmware"]["release"]) if "release" in node["software"]["firmware"] else "unknown"
-			info_fw = " firmware=" + fwinfo
-
-		if "autoupdater" in node["software"]:
-			autoupdater = node["software"]["autoupdater"]["branch"] if node["software"]["autoupdater"]["enabled"] else "off"
-			info_update = " (autoupdater="+autoupdater+")"
-
-	info_uptime = ""
-	u = -1
-	if "statistics" in node and "uptime" in node["statistics"]:
-		u = int(float(node["statistics"]["uptime"]))
-	elif 'uptime' in node:
-		u = int(float(node['uptime']))
-
-	if u > 0:
-		d, r1 = divmod(u, 86400)
-		h, r2 = divmod(r1, 3600)
-		m, s = divmod(r2, 60)
-		if d > 0:
-			info_uptime = ' up {0}d {1}h'.format(d,h)
-		elif h > 0:
-			info_uptime = ' up {0}h {1}m'.format(h,m)
-		else:
-			info_uptime = ' up {0}m'.format(m)
-
-	bot.say('[{1}]{2}{3}{4}{5}'.format(info_mac, info_name, info_hw, info_fw, info_update, info_uptime))
-
-@willie.module.commands('uptime')
-def ffpb_peeruptime(bot, trigger):
-	target_name = trigger.group(2)
-	node = ffpb_findnode_from_botparam(bot, target_name)
-	if node is None: return
-
-	info_name = node["hostname"]
-	info_uptime = ''
-	u_raw = None
-	if 'statistics' in node and 'uptime' in node['statistics']:
-		u_raw = node['statistics']['uptime']
-	elif 'uptime' in node:
-		u_raw = node['uptime']
-
-	if not u_raw is None:
-		u = int(float(u_raw))
-		d, r1 = divmod(u, 86400)
-		h, r2 = divmod(r1, 3600)
-		m, s = divmod(r2, 60)
-		if d > 0:
-			info_uptime += '{0}d '.format(d)
-		if h > 0:
-			info_uptime += '{0}h '.format(h)
-		info_uptime += '{0}m'.format(m)
-		info_uptime += ' # raw: \'{0}\''.format(u_raw)
-	else:
-		info_uptime += '?'
-
-	bot.say('uptime(\'{0}\') = {1}'.format(info_name, info_uptime))
-
-@willie.module.commands('link')
-def ffpb_peerlink(bot, trigger):
-	target_name = trigger.group(2)
-	node = ffpb_findnode_from_botparam(bot, target_name)
-	if node is None: return
-
-	info_mac = node["network"]["mac"]
-	info_name = node["hostname"]
-	info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:')
-	bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6))
-
 @willie.module.interval(60)
 def ffpb_updatepeers(bot):
 	"""Aktualisiere die Knotenliste und melde das Diff"""
@@ -457,6 +377,11 @@ def ffpb_fetch_stats(bot, url, memoryid):
 
 @willie.module.interval(15)
 def ffpb_get_stats(bot):
+	highscores = bot.memory['highscores'] if 'highscores' in bot.memory else None
+	if highscores is None:
+		print('HIGHSCORE not in bot memory')
+		return
+
 	(nodes_active, nodes_total, clients_count) = ffpb_fetch_stats(bot, 'http://map.paderborn.freifunk.net/nodes.json', 'ffpb_stats')
 	
 	highscore_changed = False
@@ -479,16 +404,6 @@ def ffpb_get_stats(bot):
 				action_target = bot.config.ffpb.msg_target_public
 			bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
 
-@willie.module.commands('status')
-def ffpb_status(bot, trigger):
-	"""Status des FFPB-Netzes: Anzahl (aktiver) Knoten + Clients"""
-	stats = bot.memory['ffpb_stats'] if 'ffpb_stats' in bot.memory else None
-	if stats is None:
-		bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?')
-		return
-
-	bot.say('Es sind {0} Knoten und ca. {1} Clients online.'.format(stats["nodes_active"], stats["clients"]))
-
 def pretty_date(time=False):
     """
     Get a datetime object or a int() Epoch timestamp and return a
@@ -532,55 +447,6 @@ def pretty_date(time=False):
         return "vor " + str(day_diff) + " Tagen"
     return "am " + compare.strftime('%d.%m.%Y um %H:%M Uhr') 
 
-@willie.module.commands('highscore')
-def ffpb_highscore(bot, trigger):
-	bot.say('Highscore: {0} Knoten ({1}), {2} Clients ({3})'.format(
-		highscores['nodes'], pretty_date(int(highscores['nodes_ts'])),
-		highscores['clients'], pretty_date(int(highscores['clients_ts']))))
-
-@willie.module.commands('rollout-status')
-def ffpb_rolloutstatus(bot, trigger):
-	result = { }
-	for branch in [ 'stable', 'testing' ]:
-		result[branch] = None
-	skipped = 0
-
-	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.')
-		return
-
-	expected_release = trigger.group(2)
-	if expected_release is None or len(expected_release) == 0:
-		bot.say('Von welcher Firmware denn?')
-		return
-
-	for nodeid in alfred_data:
-		item = alfred_data[nodeid]
-		if (not 'software' in item) or (not 'firmware' in item['software']) or (not 'autoupdater' in item['software']):
-			skipped+=1
-			continue
-
-		release = item['software']['firmware']['release']
-		branch = item['software']['autoupdater']['branch']
-		enabled = item['software']['autoupdater']['enabled']
-		if not branch in result or result[branch] is None:
-			result[branch] = { 'auto_count': 0, 'auto_not': 0, 'manual_count': 0, 'manual_not': 0, 'total': 0 }
-
-		result[branch]['total'] += 1
-		match = 'count' if release == expected_release else 'not'
-		mode = 'auto' if enabled else 'manual'
-		result[branch][mode+'_'+match] += 1
-
-	output = "Rollout von '{0}':".format(expected_release)
-	for branch in result:
-		auto_count = result[branch]['auto_count']
-		auto_total = auto_count + result[branch]['auto_not']
-		manual_count = result[branch]['manual_count']
-		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))
-	if skipped > 0:
-		bot.say("Rollout von '{0}': {1} Knoten unklar".format(expected_release, skipped))
-
 @willie.module.commands('ping')
 def ffpb_ping(bot, trigger=None, target_name=None):
 	"""Ping FFPB-Knoten"""
@@ -606,81 +472,6 @@ def ffpb_ping(bot, trigger=None, target_name=None):
 		if not bot is None: bot.say('Uh oh, irgendwas ist kaputt. Chef, ping result = ' + str(result) + ' - darf ich das essen?')
 		return None
 
-@willie.module.interval(3*60)
-def ffpb_monitor_ping(bot):
-	notify_target = bot.config.core.owner
-	if (not bot.config.ffpb.msg_target is None):
-		notify_target = bot.config.ffpb.msg_target
-
-	for node in monitored_nodes:
-		added = monitored_nodes[node][0]		
-		last_status = monitored_nodes[node][1]
-		last_check = monitored_nodes[node][2]
-
-		current_status = ffpb_ping(bot=None, target_name=node)
-		monitored_nodes[node] = ( added, current_status, time.time() )
-		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))))
-		if last_status != current_status and (last_status or current_status):
-			if last_check is None:
-				# erster Check, keine Ausgabe
-				continue
-
-			if current_status == True:
-				bot.msg(notify_target, 'Monitoring: Knoten \'{0}\' pingt wieder (zuletzt {1})'.format(node, pretty_date(last_check)))
-			else:
-				bot.msg(notify_target, 'Monitoring: Knoten \'{0}\' DOWN'.format(node))
-
-@willie.module.commands('monitor')
-def ffpb_monitor(bot, trigger):
-	if not trigger.admin:
-		bot.say('Ich ping hier nicht für jeden durch die Weltgeschichte.')
-		return
-
-	if trigger.group(2) is None or len(trigger.group(2)) == 0:
-		bot.say('Das Monitoring sagt du hast doofe Ohren.')
-		return
-
-	cmd = trigger.group(3)
-	node = trigger.group(4)
-	if not node is None: node = str(node)
-
-	if cmd == "add":
-		if node in monitored_nodes:
-			bot.say('Knoten \'{0}\' wird bereits gemonitored.'.format(node))
-			return
-		monitored_nodes[node] = ( trigger.sender, None, None )
-		bot.say('Knoten \'{0}\' wird jetzt ganz genau beobachtet.'.format(node))
-		return
-
-	if cmd == "del":
-		if not node in monitored_nodes:
-			bot.say('Knoten \'{0}\' war gar nicht im Monitoring?!?'.format(node))
-			return
-		del monitored_nodes[node]
-		bot.say('Okidoki, \'{0}\' war mir sowieso egal.'.format(node))
-		return
-
-	if cmd == "info":
-		if node in monitored_nodes:
-			info = monitored_nodes[node]
-			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]))
-		else:
-			bot.say('Knoten \'{0}\' ist nicht im Monitoring.'.format(node))
-		return
-
-	if cmd == "list":
-		nodes = ""
-		for node in monitored_nodes:
-			nodes = nodes + " " + node
-		bot.say('Monitoring aktiv für:' + nodes)
-		return
-
-	if cmd == "help":
-		bot.say('Entweder "!monitor list" oder "!monitor {add|del|info} <node>"')
-		return
-
-	bot.say('Mit "' + str(cmd) + '" kann ich nix anfangen, probier doch mal "!monitor help".')
-
 @willie.module.commands('exec-on-peer')
 def ffpb_remoteexec(bot, trigger):
 	"""Remote Execution fuer FFPB_Knoten"""

+ 94 - 0
modules/ffpb_monitoring.py

@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+from __future__ import print_function
+import willie
+
+monitored_nodes = None
+
+def setup(bot):
+	global monitored_nodes
+
+	monitored_nodes = shelve.open('monitorednodes', writeback=True)
+
+def shutdown(bot):
+	global monitored_nodes
+
+	if not monitored_nodes is None:
+		monitored_nodes.sync()
+		monitored_nodes.close()
+		monitored_nodes = None
+
+@willie.module.interval(3*60)
+def ffpb_monitor_ping(bot):
+	notify_target = bot.config.core.owner
+	if (not bot.config.ffpb.msg_target is None):
+		notify_target = bot.config.ffpb.msg_target
+
+	for node in monitored_nodes:
+		added = monitored_nodes[node][0]		
+		last_status = monitored_nodes[node][1]
+		last_check = monitored_nodes[node][2]
+
+		current_status = ffpb_ping(bot=None, target_name=node)
+		monitored_nodes[node] = ( added, current_status, time.time() )
+		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))))
+		if last_status != current_status and (last_status or current_status):
+			if last_check is None:
+				# erster Check, keine Ausgabe
+				continue
+
+			if current_status == True:
+				bot.msg(notify_target, 'Monitoring: Knoten \'{0}\' pingt wieder (zuletzt {1})'.format(node, pretty_date(last_check)))
+			else:
+				bot.msg(notify_target, 'Monitoring: Knoten \'{0}\' DOWN'.format(node))
+
+@willie.module.commands('monitor')
+def ffpb_monitor(bot, trigger):
+	if not trigger.admin:
+		bot.say('Ich ping hier nicht für jeden durch die Weltgeschichte.')
+		return
+
+	if trigger.group(2) is None or len(trigger.group(2)) == 0:
+		bot.say('Das Monitoring sagt du hast doofe Ohren.')
+		return
+
+	cmd = trigger.group(3)
+	node = trigger.group(4)
+	if not node is None: node = str(node)
+
+	if cmd == "add":
+		if node in monitored_nodes:
+			bot.say('Knoten \'{0}\' wird bereits gemonitored.'.format(node))
+			return
+		monitored_nodes[node] = ( trigger.sender, None, None )
+		bot.say('Knoten \'{0}\' wird jetzt ganz genau beobachtet.'.format(node))
+		return
+
+	if cmd == "del":
+		if not node in monitored_nodes:
+			bot.say('Knoten \'{0}\' war gar nicht im Monitoring?!?'.format(node))
+			return
+		del monitored_nodes[node]
+		bot.say('Okidoki, \'{0}\' war mir sowieso egal.'.format(node))
+		return
+
+	if cmd == "info":
+		if node in monitored_nodes:
+			info = monitored_nodes[node]
+			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]))
+		else:
+			bot.say('Knoten \'{0}\' ist nicht im Monitoring.'.format(node))
+		return
+
+	if cmd == "list":
+		nodes = ""
+		for node in monitored_nodes:
+			nodes = nodes + " " + node
+		bot.say('Monitoring aktiv für:' + nodes)
+		return
+
+	if cmd == "help":
+		bot.say('Entweder "!monitor list" oder "!monitor {add|del|info} <node>"')
+		return
+
+	bot.say('Mit "' + str(cmd) + '" kann ich nix anfangen, probier doch mal "!monitor help".')
+

+ 94 - 0
modules/ffpb_nodeinfos.py

@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+from __future__ import print_function
+import willie
+
+def setup(bot):
+	pass
+
+@willie.module.commands('info')
+def ffpb_peerinfo(bot, trigger):
+	target_name = trigger.group(2)
+	node = ffpb_findnode_from_botparam(bot, target_name)
+	if node is None: return
+
+	info_mac = node["network"]["mac"]
+	info_name = node["hostname"]
+
+	info_hw = ""
+	if "hardware" in node:
+		if "model" in node["hardware"]:
+			model = node["hardware"]["model"]
+			info_hw = " model='" + model + "'"
+
+	info_fw = ""
+	info_update = ""
+	if "software" in node:
+		if "firmware" in node["software"]:
+			fwinfo = str(node["software"]["firmware"]["release"]) if "release" in node["software"]["firmware"] else "unknown"
+			info_fw = " firmware=" + fwinfo
+
+		if "autoupdater" in node["software"]:
+			autoupdater = node["software"]["autoupdater"]["branch"] if node["software"]["autoupdater"]["enabled"] else "off"
+			info_update = " (autoupdater="+autoupdater+")"
+
+	info_uptime = ""
+	u = -1
+	if "statistics" in node and "uptime" in node["statistics"]:
+		u = int(float(node["statistics"]["uptime"]))
+	elif 'uptime' in node:
+		u = int(float(node['uptime']))
+
+	if u > 0:
+		d, r1 = divmod(u, 86400)
+		h, r2 = divmod(r1, 3600)
+		m, s = divmod(r2, 60)
+		if d > 0:
+			info_uptime = ' up {0}d {1}h'.format(d,h)
+		elif h > 0:
+			info_uptime = ' up {0}h {1}m'.format(h,m)
+		else:
+			info_uptime = ' up {0}m'.format(m)
+
+	bot.say('[{1}]{2}{3}{4}{5}'.format(info_mac, info_name, info_hw, info_fw, info_update, info_uptime))
+
+@willie.module.commands('uptime')
+def ffpb_peeruptime(bot, trigger):
+	target_name = trigger.group(2)
+	node = ffpb_findnode_from_botparam(bot, target_name)
+	if node is None: return
+
+	info_name = node["hostname"]
+	info_uptime = ''
+	u_raw = None
+	if 'statistics' in node and 'uptime' in node['statistics']:
+		u_raw = node['statistics']['uptime']
+	elif 'uptime' in node:
+		u_raw = node['uptime']
+
+	if not u_raw is None:
+		u = int(float(u_raw))
+		d, r1 = divmod(u, 86400)
+		h, r2 = divmod(r1, 3600)
+		m, s = divmod(r2, 60)
+		if d > 0:
+			info_uptime += '{0}d '.format(d)
+		if h > 0:
+			info_uptime += '{0}h '.format(h)
+		info_uptime += '{0}m'.format(m)
+		info_uptime += ' # raw: \'{0}\''.format(u_raw)
+	else:
+		info_uptime += '?'
+
+	bot.say('uptime(\'{0}\') = {1}'.format(info_name, info_uptime))
+
+@willie.module.commands('link')
+def ffpb_peerlink(bot, trigger):
+	target_name = trigger.group(2)
+	node = ffpb_findnode_from_botparam(bot, target_name)
+	if node is None: return
+
+	info_mac = node["network"]["mac"]
+	info_name = node["hostname"]
+	info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:')
+	bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6))
+

+ 71 - 0
modules/ffpb_status.py

@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+from __future__ import print_function
+import willie
+
+def setup(bot):
+	pass
+
+@willie.module.commands('status')
+def ffpb_status(bot, trigger):
+	"""Status des FFPB-Netzes: Anzahl (aktiver) Knoten + Clients"""
+	stats = bot.memory['ffpb_stats'] if 'ffpb_stats' in bot.memory else None
+	if stats is None:
+		bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?')
+		return
+
+	bot.say('Es sind {0} Knoten und ca. {1} Clients online.'.format(stats["nodes_active"], stats["clients"]))
+
+@willie.module.commands('highscore')
+def ffpb_highscore(bot, trigger):
+	highscores = bot.memory['highscores'] if 'highscores' in bot.memory else None
+	if highscores is None:
+		bot.reply('Sorry, ich habe gerade keine Highscore-Daten parat. Und würfeln ist auch eher uncool.')
+		return
+
+	bot.say('Highscore: {0} Knoten ({1}), {2} Clients ({3})'.format(
+		highscores['nodes'], pretty_date(int(highscores['nodes_ts'])),
+		highscores['clients'], pretty_date(int(highscores['clients_ts']))))
+
+@willie.module.commands('rollout-status')
+def ffpb_rolloutstatus(bot, trigger):
+	result = { }
+	for branch in [ 'stable', 'testing' ]:
+		result[branch] = None
+	skipped = 0
+
+	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.')
+		return
+
+	expected_release = trigger.group(2)
+	if expected_release is None or len(expected_release) == 0:
+		bot.say('Von welcher Firmware denn?')
+		return
+
+	for nodeid in alfred_data:
+		item = alfred_data[nodeid]
+		if (not 'software' in item) or (not 'firmware' in item['software']) or (not 'autoupdater' in item['software']):
+			skipped+=1
+			continue
+
+		release = item['software']['firmware']['release']
+		branch = item['software']['autoupdater']['branch']
+		enabled = item['software']['autoupdater']['enabled']
+		if not branch in result or result[branch] is None:
+			result[branch] = { 'auto_count': 0, 'auto_not': 0, 'manual_count': 0, 'manual_not': 0, 'total': 0 }
+
+		result[branch]['total'] += 1
+		match = 'count' if release == expected_release else 'not'
+		mode = 'auto' if enabled else 'manual'
+		result[branch][mode+'_'+match] += 1
+
+	output = "Rollout von '{0}':".format(expected_release)
+	for branch in result:
+		auto_count = result[branch]['auto_count']
+		auto_total = auto_count + result[branch]['auto_not']
+		manual_count = result[branch]['manual_count']
+		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))
+	if skipped > 0:
+		bot.say("Rollout von '{0}': {1} Knoten unklar".format(expected_release, skipped))
+