Helge Jung %!s(int64=10) %!d(string=hai) anos
pai
achega
f3ba704401
Modificáronse 1 ficheiros con 122 adicións e 10 borrados
  1. 122 10
      modules/ffpb.py

+ 122 - 10
modules/ffpb.py

@@ -34,6 +34,8 @@ 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])
@@ -56,6 +58,9 @@ class MsgHandler(SocketServer.BaseRequestHandler):
 		bot.msg(target, "[{0}] {1}".format(sender, str(data)))
 
 	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."):
 			return "localhost"
 
@@ -71,8 +76,10 @@ class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
 def setup(bot):
 	global msgserver, peers_repo, alfred_method, highscores, monitored_nodes
 
+	# signal begin of setup routine
 	bot.memory['ffpb_in_setup'] = True
 
+	# load highscores from disk
 	highscores = shelve.open('highscoredata', writeback=True)
 	if not 'nodes' in highscores:
 		highscores['nodes'] = 0
@@ -81,19 +88,24 @@ def setup(bot):
 		highscores['clients'] = 0
 		highscores['clients_ts'] = time.time()
 
+	# load list of monitored nodes and their last status from disk
 	monitored_nodes = shelve.open('nodes.monitored', writeback=True)
 
+	# load list of seen nodes from disk
 	seen_nodes = shelve.open('nodes.seen', writeback=True)
 	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'):
 		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
@@ -109,29 +121,35 @@ def setup(bot):
 		msgserver_thread.daemon = True
 		msgserver_thread.start()
 
+	# initially fetch ALFRED data
 	alfred_method = bot.config.ffpb.alfred_method
 	ffpb_updatealfred(bot)
 
+	# signal end of setup routine
 	bot.memory['ffpb_in_setup'] = False
 
 def shutdown(bot):
 	global msgserver, highscores, monitored_nodes
 
+	# store highscores
 	if not highscores is None:
 		highscores.sync()
 		highscores.close()
 		highscores = None
 
+	# store monitored nodes
 	if not monitored_nodes is None:
 		monitored_nodes.sync()
 		monitored_nodes.close()
 		monitored_nodes = 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.")
@@ -141,14 +159,17 @@ def shutdown(bot):
 @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())))
@@ -164,6 +185,9 @@ def ffpb_help(bot, trigger):
 	bot.say("Allgemeine Hilfe gibt's mit !help - ohne Parameter.")
 
 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:
 		return None
 
@@ -178,7 +202,7 @@ def ffpb_findnode(name):
 		if mac in alfred_data:
 			return alfred_data[mac]
 
-		# try to find alias MAC
+		# try to find alias MAC in ALFRED data
 		for nodeid in alfred_data:
 			node = alfred_data[nodeid]
 			if "network" in node:
@@ -205,7 +229,7 @@ def ffpb_findnode(name):
 			else:
 				names[h] = nodeid
 
-	# still not found -> try peers_repo
+	# not found in ALFRED data -> try peers_repo
 	if not peers_repo is None:
 		peer_name = None
 		peer_mac = None
@@ -238,9 +262,12 @@ def ffpb_findnode(name):
 		# if we got exactly one candidate that might be it
 		return 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
@@ -256,10 +283,13 @@ def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata = True):
 	node = ffpb_findnode(name)
 	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:]
@@ -332,7 +362,6 @@ def ffpb_updatealfred(bot):
 			action_target = bot.config.ffpb.msg_target
 			bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
 
-
 def ffpb_alfred_data_outdated():
 	timeout = datetime.datetime.now() - datetime.timedelta(minutes=5)
 	is_outdated = timeout > alfred_update
@@ -341,6 +370,8 @@ def ffpb_alfred_data_outdated():
 
 @willie.module.commands('debug-alfred')
 def ffpb_debug_alfred(bot, trigger):
+	"""Show statistics of available ALFRED data."""
+
 	if alfred_data is None:
 		bot.say("Keine ALFRED-Daten vorhanden.")
 	else:
@@ -348,28 +379,38 @@ def ffpb_debug_alfred(bot, trigger):
 
 @willie.module.commands('alfred-data')
 def ffpb_peerdata(bot, trigger):
+	"""Show ALFRED data of the given node."""
+
+	# user must be a bot admin
 	if (not trigger.admin):
 		bot.say('I wont leak (possibly) sensitive data to you.')
 		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]):
 		bot.say('Kein Keks? Keine Daten.')
 		return
 
+	# identify node or bail out
 	target_name = trigger.group(2)
 	node = ffpb_findnode_from_botparam(bot, target_name)
 	if node is None: return
 
+	# reply each key in the node's data
 	for key in node:
 		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):
+	"""Show information of the given node."""
+
+	# identify node or bail out
 	target_name = trigger.group(2)
 	node = ffpb_findnode_from_botparam(bot, target_name)
 	if node is None: return
 
+	# read node information
 	info_mac = node["network"]["mac"]
 	info_name = node["hostname"]
 
@@ -412,10 +453,14 @@ def ffpb_peerinfo(bot, trigger):
 
 @willie.module.commands('uptime')
 def ffpb_peeruptime(bot, trigger):
+	"""Display the uptime of the given node."""
+
+	# identify node or bail out
 	target_name = trigger.group(2)
 	node = ffpb_findnode_from_botparam(bot, target_name)
 	if node is None: return
 
+	# get name and raw uptime from node
 	info_name = node["hostname"]
 	info_uptime = ''
 	u_raw = None
@@ -424,6 +469,7 @@ def ffpb_peeruptime(bot, trigger):
 	elif 'uptime' in node:
 		u_raw = node['uptime']
 
+	# pretty print uptime
 	if not u_raw is None:
 		u = int(float(u_raw))
 		d, r1 = divmod(u, 86400)
@@ -438,22 +484,32 @@ def ffpb_peeruptime(bot, trigger):
 	else:
 		info_uptime += '?'
 
+	# reply to user
 	bot.say('uptime(\'{0}\') = {1}'.format(info_name, info_uptime))
 
 @willie.module.commands('link')
 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)
 	node = ffpb_findnode_from_botparam(bot, target_name)
 	if node is None: return
 
+	# get node's MAC
 	info_mac = node["network"]["mac"]
 	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:')
+
+	# reply to user
 	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"""
+	"""Refresh list of peers and message the diff."""
+
 	if peers_repo is None:
 		print('WARNING: peers_repo is None')
 		return
@@ -495,6 +551,9 @@ def ffpb_updatepeers(bot):
 		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)
@@ -529,8 +588,10 @@ def ffpb_fetch_stats(bot, url, memoryid):
 
 @willie.module.interval(15)
 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')
-	
+
 	highscore_changed = False
 	if nodes_active > highscores['nodes']:
 		highscores['nodes'] = nodes_active
@@ -553,7 +614,8 @@ def ffpb_get_stats(bot):
 
 @willie.module.commands('status')
 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
 	if stats is None:
 		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')
 def ffpb_rolloutstatus(bot, trigger):
+	"""Display statistic on how many nodes have installed the given firmware version."""
+
+	# initialize results dictionary
 	result = { }
 	for branch in [ 'stable', 'testing' ]:
 		result[branch] = None
 	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]):
 		bot.say('Geh zur dunklen Seite, die haben Kekse - ohne Keks kein Rollout-Status.')
 		return
 
+	# read expected firmware version from command arguments
 	expected_release = trigger.group(2)
 	if expected_release is None or len(expected_release) == 0:
 		bot.say('Von welcher Firmware denn?')
 		return
 
+	# check each node in ALFRED data
 	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']):
@@ -643,6 +711,7 @@ def ffpb_rolloutstatus(bot, trigger):
 		mode = 'auto' if enabled else 'manual'
 		result[branch][mode+'_'+match] += 1
 
+	# respond to user
 	output = "Rollout von '{0}':".format(expected_release)
 	for branch in result:
 		auto_count = result[branch]['auto_count']
@@ -650,19 +719,26 @@ def ffpb_rolloutstatus(bot, trigger):
 		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))
+
+	# output count of nodes for which the autoupdater's branch and/or 
+	# firmware version could not be retrieved
 	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"""
+	"""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:
@@ -680,10 +756,14 @@ def ffpb_ping(bot, trigger=None, target_name=None):
 
 @willie.module.interval(3*60)
 def ffpb_monitor_ping(bot):
+	"""Ping each node currently under surveillance."""
+
+	# determine where-to to send alerts
 	notify_target = bot.config.core.owner
 	if (not bot.config.ffpb.msg_target is None):
 		notify_target = bot.config.ffpb.msg_target
 
+	# check each node under surveillance
 	for node in monitored_nodes:
 		mon = monitored_nodes[node]
 		added = mon['added']
@@ -709,18 +789,24 @@ def ffpb_monitor_ping(bot):
 
 @willie.module.commands('monitor')
 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:
 		bot.say('Ich ping hier nicht für jeden durch die Weltgeschichte.')
 		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:
 		bot.say('Das Monitoring sagt du hast doofe Ohren.')
 		return
 
+	# read additional arguments
 	cmd = trigger.group(3)
 	node = trigger.group(4)
 	if not node is None: node = str(node)
 
+	# subcommand 'add': add a node to monitoring
 	if cmd == "add":
 		if node in monitored_nodes:
 			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))
 		return
 
+	# subcommand 'del': remote a node from monitoring
 	if cmd == "del":
 		if not node in monitored_nodes:
 			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))
 		return
 
+	# subcommand 'info': monitoring status of a node
 	if cmd == "info":
 		if node in monitored_nodes:
 			info = monitored_nodes[node]
@@ -752,6 +840,7 @@ def ffpb_monitor(bot, trigger):
 			bot.say('Knoten \'{0}\' ist nicht im Monitoring.'.format(node))
 		return
 
+	# subcommand 'list': enumerate all monitored nodes
 	if cmd == "list":
 		nodes = ""
 		for node in monitored_nodes:
@@ -759,40 +848,53 @@ def ffpb_monitor(bot, trigger):
 		bot.say('Monitoring aktiv für:' + nodes)
 		return
 
+	# subcommand 'help': give some hints what the user can do
 	if cmd == "help":
 		bot.say('Entweder "!monitor list" oder "!monitor {add|del|info} <node>"')
 		return
 
+	# no valid subcommand given: complain
 	bot.say('Mit "' + str(cmd) + '" kann ich nix anfangen, probier doch mal "!monitor help".')
 
 @willie.module.commands('providers')
 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.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]]))
 
 @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: nodeid = node['network']['mac'].replace(':','') if 'network' in node and 'mac' in node['network'] else 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))
 		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)))
 
+	# query BATCAVE for neighbour's names
 	d = '&'.join([ str(n) for n in cave_result ])
 	req = urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/idmac2name', d)
 
+	# filter out duplicate names
 	neighbours = set()
 	for n in req:
 		ident,name = n.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:
@@ -802,7 +904,8 @@ def ffpb_nodemesh(bot, trigger):
 
 @willie.module.commands('exec-on-peer')
 def ffpb_remoteexec(bot, trigger):
-	"""Remote Execution fuer FFPB_Knoten"""
+	"""Remote execution on the given node"""
+
 	bot_params = trigger.group(2).split(' ',1)
 	if len(bot_params) != 2:
 		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_cmd = bot_params[1]
 
+	# remote execution may only be triggered by bot admins
 	if not trigger.admin:
 		bot.say('I can haz sudo?')
 		return
 
+	# make sure remote execution is done in public
 	if trigger.is_privmsg:
 		bot.say('Bitte per Channel.')
 		return
 
+	# double-safety: user must be op in the channel, too (hoping for NickServ authentication)
 	if not trigger.nick in bot.ops[trigger.sender]:
 		bot.say('Geh weg.')
 		return
 
+	# identify requested node or bail out
 	node = ffpb_findnode_from_botparam(bot, target_name, ensure_recent_alfreddata=False)
 	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_alias = node["hostname"]
 
+	# assemble SSH command
 	cmd = 'ssh -6 -l root ' + target + ' -- "' + target_cmd + '"'
 	print("REMOTE EXEC = " + cmd)
 	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)
+
+		# fetch results and sent at most 8 of them as response
 		lines = str(result).splitlines()
 
 		if len(lines) == 0: