#! /usr/bin/python
# -*- coding: utf-8
#
# Written by Mario Kicherer, 2007 (http://empanyc.net)
#
# Monitors the number of booted operating systems and/or logged-in users
# -------------------------------------------------------------------
#
# Commandline parameters:
#
#	- debug : creates readable debug messages
#
# possible environment parameters:
#
#	env.iprange - ip range that shall be scanned
#	env.snmp_community - the community id used for snmp requests (default: public)
#	env.runmode - "usage", "booted" or "both" (in "both" mode the script monitors the "usage" 
#			and the "booted" value of the _first_ os in the systems variable)
#	env.systems - space seperated list of OSNAMEs or "builtin" ("builtin" monitors winxp and linux)
#	env.OSNAME_regex - for a custom regex
#	env.OSNAME_title - for a custom title
#	env.OSNAME_family - "windows" or "" (needed for an alternative user counting on windows systems, because normal method doesn't work...)
#	env.OSNAMEusers_draw - draw style for runmode "usage" or "both" (f.e. env.linuxusers_draw AREA, default LINE1)
#	env.OSNAME_draw - draw style for runmode "booted" or "both" (f.e. env.windows_draw AREA, default LINE1)
#
#	env.total_draw - draw the total amount of operating systems or users with draw style (f.e. env.total_draw LINE1), if empty no total value will
#				be drawn
#	env.host_name - optional hostname to display statistic in a seperated section (f.e. a special "Pool" section on the munin site)
#
# This script needs read permission for the following three SNMP entries:
#	- sysDescr.0 (in every case)
#	- hrSystemNumUsers.0 (only for user counting)
#	- hrSWRunName (only for user counting on windows systems)
#
# Magic markers
#%# capabilities=autoconf
#%# family=contrib

import os,re,string,sys,thread
from threading import Thread

def print_autoconf():
        print "yes"

def print_config():
	global ip_range;
	global snmp_community;
	global systems;
	global runmode;
	global total_draw;
	global host_name;
	
	global system_title;
	global system_regex;
	global system_draw;
	
	if (not host_name == ""):
		print "host_name "+host_name
	
	if (runmode == "booted"):
		print "graph_title Operating systems booted"
		print "graph_vlabel Number of PCs"

	if (runmode == "usage"):
		print "graph_title Operating systems used"
		print "graph_vlabel Number of users"
		
	if (runmode == "both"):
		print "graph_title "+system_title[systems[0]]+" details"
		print "graph_vlabel Number of PCs/users"
	
	print "graph_category Clients"
	print "graph_args --lower-limit 0"
	
	systems.sort();
	for os_type in systems:
		if (runmode == "booted") or (runmode == "both"):
			print os_type.lower() + ".label " + system_title[os_type] + " booted"
			print os_type.lower() + ".draw " + system_draw[os_type]

		if (runmode == "usage") or (runmode == "both"):
			print os_type.lower() + "users.label using " + system_title[os_type]
			print os_type.lower() + "users.draw " + system_draw[os_type]
	
	## shall the total amount be drawn
	if total_draw != "":
		print "total.label Total"
		print "total.draw " + total_draw;

def alternative_getusers_for_windows(ip):
	############################
	### alternative method to get the logged in users in windows
	#
	# The normal snmp query under windows returns strange values.
	# This method counts the number of explorer.exe processes, because
	# every logged in users has one.
	stdout = os.popen(snmpwalk_cmd + ip + " 1.3.6.1.2.1.25.4.2.1.2")
	snmp_result = stdout.readlines()
	stdout.close();
	
	answer = 0;
	alt_users = 0;
	for line in snmp_result:
		result = re.search('HOST-RESOURCES-MIB::hrSWRunName\.[\d]+\s=\s\"([^\"]+)', line)
		if result:
			answer = 1;
			if result.group(1) == "explorer.exe":
				alt_users += 1;
	if (answer):
		return alt_users
	else:
		return -1

def getusers(ip):
	##############################
	### query the logged in users
	
	stdin, stdout, stderr = os.popen3(snmpwalk_cmd + ip + " hrSystemNumUsers.0")
	snmp_result = string.join(stdout.readlines())
	stdin.close();
	stdout.close();
	stderr.close();
	
	users = "-1"
	result = re.search('HOST-RESOURCES-MIB::hrSystemNumUsers.0\s=\s(\d)', snmp_result)
	if result:
		users = result.group(1)
	return int(users)
	
class ScanThread(Thread):
	def __init__(self, debug, ip):
		Thread.__init__(self)
		self.debug = debug
		self.ip = ip
		
	def run(self):
		
		global systems;
		global runmode;
		
		global os_booted;
		global os_users;
		global snmpwalk_cmd;
		global system_family;
		
		############################
		### query system description
		
		stdin, stdout, stderr = os.popen3(snmpwalk_cmd + self.ip + " sysDescr.0")
		snmp_result = string.join(stdout.readlines())
		errors = string.join(stderr.readlines())
		stdin.close()
		stdout.close()
		stderr.close()
		
		users = 0
		os_type = "unknown"
		found = 0
		
		if re.search('Timeout:.+', errors):
			if self.debug:
				print "snmpd is not responding";
		else:
			# determine booted os in every case - needed for correct user counting
			os_type = "";
			for system in systems:
				result = re.search(system_regex[system], snmp_result)
				if result:
					os_type = system;
					if (runmode == "booted") or (runmode == "both"):
						found = 1;
			
			# counting logged in users, too?
			if (os_type != "") and ((runmode == "usage") or (runmode == "both")):
				if (system_family[os_type] == "windows"):
					users = alternative_getusers_for_windows(self.ip)
					if users > -1:
						found = 1
				if (system_family[os_type] == ""):
					users = getusers(self.ip)
					if users > -1:
						found = 1
			
			# did the snmpd returned the needed values?
			if found:
				if self.debug:
					print " OS: %s" % (os_type),
					if (runmode == "usage") or (runmode == "both"):
						print " with %i users" % (users)
		
				##############################
				## append data 
		
				# append os_type if not in dictionary
				datalock.acquire()
				if not os_users.has_key(os_type):
					os_users[os_type] = 0
					os_booted[os_type] = 0
		
				# a user logged in?
				# if logged in users > 0 -> counter+1 
				if (int(users) > 0):
					os_users[os_type] = os_users[os_type] + 1
				
				# increase the os counter
				os_booted[os_type] = os_booted[os_type] + 1
				datalock.release()
				# simple newline...
				if self.debug:
					print ""
			else:
				if self.debug:
					print "chosen os not found."


def scan(debug):
	global systems;
	global runmode;
	
	global os_booted;
	global os_users;
	
	threadlist = []

	if len(systems) <= 0:
		if debug:
			print "please choose an operating system."
		return os_booted, os_users
	else:
		if debug:
			print "OS: ",
			for system in systems:
				print system,
			print ""
	
	if runmode == "":
		if debug:
			print "please choose a monitoring option."
		return os_booted, os_users
	else:
		if debug:
			print "Monitoring: " + runmode
	
	### using nmap to scan for running pcs
	result = os.popen("nmap --randomize_hosts -sP " + ip_range + " | grep Host")
	nmap_result = result.readlines();
	result.close();
	
	### for each pc do...
	for line in nmap_result:
		### returns the ip
		ip = re.search('Host[^\(]+\(([\d{1,3}\|\.]+)\) appears to be up.', line)
		if debug:
			print ip.group(1) + " is up, starting snmp query...",
		
		if ip:
			# start a thread for every ip
			current = ScanThread(debug,ip.group(1))
			threadlist.append(current)
			current.start()
	
	for thr in threadlist:
		# wait for every thread
		thr.join()
	
	if debug:
		for opsys in os_users.keys():
			if (runmode == "usage") or (runmode == "all"):
				print "%s has %i users." % (opsys, os_users[opsys])
			if (runmode == "booted") or (runmode == "all"):
				print "%i pcs running with %s" % (os_booted[opsys], opsys)

	return os_booted, os_users

def read_config():
	global ip_range;
	global snmp_community;
	global systems;
	global runmode;
	global total_draw;
	global snmpwalk_cmd;
	global host_name;
	
	global system_title;
	global system_regex;
	global system_draw;
	global system_family;
	
	##################
	## read configuration from environment variables
	
	if "iprange" in os.environ.keys():
		ip_range = os.environ["iprange"]
	else:
		ip_range = ""
		ip_range = "141.3.12.100-200";
		if debug:
			print "Please specify an IP range"
		#sys.exit(1)
	
	if "snmp_community" in os.environ.keys():
		snmp_community = os.environ["snmp_community"]
	else:
		snmp_community = "public"
		snmp_community = "i08"
		if debug:
			print "using default community: " + snmp_community
	snmpwalk_cmd = "snmpwalk -v1 -OQ -t 1 -r 0 -c " + snmp_community + " "
	
	if "systems" in os.environ.keys():
		systems = os.environ["systems"]
	else:
		systems = "builtin"
		if debug:
			print "OS: " + systems
	
	if "runmode" in os.environ.keys():
		runmode = os.environ["runmode"]
	else:
		runmode = "usage"
		if debug:
			print "Monitoring: " + runmode
		
	if "total_draw" in os.environ.keys():
		total_draw = os.environ["total_draw"]
	else:
		total_draw = ""
		if debug:
			print "Total draw: " + total_draw
	
	if "host_name" in os.environ.keys():
		host_name = os.environ["host_name"]
	else:
		host_name = ""
		if debug:
			print "Hostname: " + host_name
	
	if systems != "builtin":
		systems = systems.split(" ")
		systems.sort();
		set_standard_config()
		read_system_config()
	else:
		systems = ["linux","winxp"]
		system_title["winxp"] = "Windows XP"
		system_regex["winxp"] = "SNMPv2-MIB::sysDescr\.0\s=\s.*Software:\sWindows.*\sVersion\s(5\.1).*"
		system_draw["winxp"] = "LINE1"
		system_family["winxp"] = "windows";
		
		system_title["linux"] = "Linux"
		system_regex["linux"] = "SNMPv2-MIB::sysDescr\.0\s=\s(Linux).*"
		system_draw["linux"] = "LINE1"
		system_family["linux"] = "";
		
		read_system_config()

	if runmode == "both":
		new_systems = systems[0];
		systems = [new_systems];

def read_system_config():
	global system_title;
	global system_regex;
	global system_draw;
	global system_family;
	global systems;
	
	for system in systems:
		if system+"_title" in os.environ.keys():
			system_title[system] = os.environ[system+"_title"]
			
		if system+"_regex" in os.environ.keys():
			system_regex[system] = os.environ[system+"_regex"]
			
		if system+"_draw" in os.environ.keys():
			system_draw[system] = os.environ[system+"_draw"]
		
		if system+"_family" in os.environ.keys():
			system_family[system] = os.environ[system+"_family"]

def set_standard_config():
	global system_title;
	global system_regex;
	global system_draw;
	global system_family;
	global systems;
	
	for system in systems:
		system_title[system] = system
		system_regex[system] = ""
		system_draw[system] = "LINE1"
		system_family[system] = ""

#######################
### main

debug = 0
systems = ""
runmode = ""
snmpwalk_cmd = ""
host_name = ""
os_users = {}
os_booted = {}

systems = {}
system_title = {}
system_regex = {}
system_draw = {}
system_family = {}
total_draw = "";

datalock=thread.allocate_lock()

#################
## prefer the debug flag, because its propably needed for the other parameters
if "debug" in sys.argv:
	debug = 1

read_config()

#################
#### parse the commandline parameters
for parm in sys.argv:
	if parm == "autoconf":
		print_autoconf()
		sys.exit(0)
	if parm == "config":
		print_config()
		sys.exit(0)

os_booted, os_users = scan(debug)

total = 0

for os_type in systems:
	if not os_booted.has_key(os_type):
		os_booted[os_type] = 0;
	if not os_users.has_key(os_type):
		os_users[os_type] = 0;
	if (runmode == "booted") or (runmode == "both"):
		print os_type.lower() + ".value %i" % (os_booted[os_type])
		total += os_booted[os_type]
	if (runmode == "usage") or (runmode == "both"):
		print os_type.lower() + "users.value %i" % (os_users[os_type])
		total += os_users[os_type]
	
if "total_draw" in os.environ.keys():
	print "total.value %i" % (total)

