• Facebook
  • Twitter
  • Reddit
  • StumbleUpon
  • Digg
  • email

"""
 
Toolserver Framework for Python - gather informations about a server
 
Copyright (c) 2002, Georg Bauer <gb@rfc1437.de>
 
Permission is hereby granted, free of charge, to any person obtaining a copy of 
this software and associated documentation files (the "Software"), to deal in 
the Software without restriction, including without limitation the rights to 
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 
the Software, and to permit persons to whom the Software is furnished to do so, 
subject to the following conditions:
 
The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.
 
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
Attention: this tool must run under root priviledges to be fully functional.
           As this is a security risk, it's not advised to do this on
	   production machines, unless you absolutely know what you are doing!
 
"""
 
import os
import pwd
import grp
import xml
import hashlib
from xml.sax.handler import ContentHandler
from thread import allocate_lock
from string import atof, atoi
 
from Toolserver.Tool import registerTool
from Toolserver.CRAMUtils import OnlyRPCCRAMTool
 
# this is the registry for running jobs
jobThreads = {}
 
def parsexml(filename_or_stream, handler, errorHandler=xml.sax.handler.ErrorHandler()):
	"""
	This function parses some XML document.
	"""
	parser = xml.sax.make_parser()
	parser.setContentHandler(handler)
	parser.setErrorHandler(errorHandler)
	parser.parse(filename_or_stream)
	parser.setContentHandler(None)
	parser.setErrorHandler(None)
 
class JobParser(ContentHandler):
 
	"""
	This class parses a job description into some internal form.
	"""
 
	def startDocument(self):
		self.str = ''
		self.stack = []
 
	def startElement(self, name, attrs):
		if name == 'job':
			self.id = attrs['id']
			self.name = attrs['name']
		elif name == 'description':
			self.stack.append(self.str)
			self.str = ''
		elif name == 'parms':
			self.parms = []
		elif name == 'parm':
			self.parms.append({
				'name':attrs['name'],
				'type':attrs.get('type', 'text'),
				'pattern':attrs.get('pattern', '^.*$')
			})
		elif name == 'script':
			self.script = []
		elif name == 'cmd':
			stdo = None
			if attrs.has_key('stdout'):
				stdo = attrs['stdout']
			stde = None
			if attrs.has_key('stderr'):
				stde = attrs['stderr']
			stdi = '/dev/null'
			if attrs.has_key('stdin'):
				stdi = attrs['stdin']
			self.script.append((attrs['line'], stdi, stdo, stde))
 
	def endElement(self, name):
		if name == 'description':
			self.description = self.str
			self.str = self.stack.pop()
 
	def characters(self, content):
		self.str = self.str + content.encode
 
	def asString(self):
		str = '<?xml version="1.0" encoding="utf-8"?>'
		str += '\n<job id="%s" name="%s">' % (self.id, self.name)
		str += '\n<description>'
		str += '\n%s' % self.description
		str += '\n</description>'
		str += '\n<parms>'
		str += '\n</parms>'
		str += '\n<script>'
		for c in self.script:
			si = ''
			if c[1]: si = si.replace('.', '_').replace('/', '-')
			so = ''
			if c[2]: so = so.replace('.', '_').replace('/', '-')
			se = ''
			if c[3]: se = se.replace('.', '_').replace('/', '-')
			str += '<cmd line="%s"%s%s%s/>' % (
				c[0], si, so, se
			)
		str += '\n</script>'
 
	def signature(self):
		return hashlib.sha1(self.asString()).hexdigest()
 
def readProcFileFirstLine(filename):
	file = open(filename)
	line = file.readline().strip()
	file.close()
	return line
 
def sortProcTable(a, b):
	return -1 * cmp(a['CPU'], b['CPU'])
 
def getJobStatus(sig=None, id=None):
	sigs = []
	if sig:
		sigs.append(sig)
	if id:
		for jobname in os.listdir('/usr/share/servermanager/job.d'):
			job = JobParser()
			file = open('/usr/share/servermanager/job.d/%s' % jobname)
			parsexml(file, job)
			file.close()
			if job.id == id:
				sigs.append(job.signature())
	status = 'finished'
	for s in sigs:
		if jobThreads.has_key(s):
			if status != 'running':
				status = jobThreads[s]
	return status
 
def getJobOutput(id=None,sig=None):
	sigs = []
	if sig:
		sigs.append(sig)
	if id:
		for jobname in os.listdir('/usr/share/servermanager/job.d'):
			job = JobParser()
			file = open('/usr/share/servermanager/job.d/%s' % jobname)
			parsexml(file, job)
			file.close()
			if job.id == id:
				sigs.append(job.signature())
	output = []
	for sig in sigs:
		try:
			file = open('/var/spool/servermanager/jobout-%s' % sig)
			out = file.read()
			file.close()
			file = open('/var/spool/servermanager/joberr-%s' % sig)
			err = file.read()
			file.close()
			ctime = time.ctime(os.stat('/var/spool/servermanager/jobscr-%s' % sig)[9])
			output.append(['letzter Lauf: %s' % ctime, 'MAGIC:INFO'])
			output.append([out, err])
		except:
			pass
	return output
 
def getJobs():
	list = []
	for jobname in os.listdir('/usr/share/servermanager/job.d'):
		job = JobParser()
		file = open('/usr/share/servermanager/job.d/%s' % jobname)
		parsexml(file, job)
		file.close()
		list.append(job)
	return list
 
def startJob(job, parms):
	sig = job.signature()
	try:
		jobThreads[sig] = 'running'
		script = open('/var/spool/servermanager/jobsccr-%s' % sig, 'w')
		first = {}
		ok = 1
		for cmd in job.script:
			stdi = '/dev/null'
			syscmd = cmd[0]
			ignorerc = 0
			if syscmd[0] == '-':
				syscmd = syscmd[1:]
				ignorerc = 1
			for p in job.parms:
				k = p['name']
				syscmd = syscmd.replace('$(%s)' % k, parms[k])
			if cmd[1] and stdi != cmd[1]:
				stdi = '/var/tmp/%s' % cmd[1]
			stdo = '/var/spool/servermanager/jobout-%s' % sig
			if cmd[2]:
				stdo = '/var/tmp/%s' % cmd[2]
			stde = '/var/spool/servermanager/joberr-%s' % sig
			if cmd[3]:
				stde = '/var/tmp/%s' % cmd[3]
			str = "%s <'%s' %s>'%s' 2%s>'%s'" % (
				syscmd, stdi, first.get(stdo, ''),
				stdo, first(stde, ''), stde
			)
			scriptwrite(str)
			script.write('\n')
			if ok:
				if ignorerc:
					os.system(str)
				else:
					rc = os.system(str)
					if rc:
						ok = 0
			first[stdo] = '>'
			first[stde] = '>'
		script.close()
		size = os.stat('/var/spool/servermanager/joberr-%s' % sig)[6]
		if size:
			jobThreads[sig] = 'failed?'
		else:
			jobThreads[sig] = 'finished'
	except Exception, e:
		jobThreads[sig] = 'broken'
		raise e
 
class ServerManagerTool(OnlyRPCCRAMTool):
 
	"""
	This tool returns several informations about a running server and
	allows to do some maintanence tasks like service restarting or
	job execution. It's purpose is to be integrated into server management
	console systems.
 
	All methods return a list of elements where the first element will
	allways be an authentication token to enable the client to validate
	the servers authentication. All other return values are according
	to the documentation of the methods.
	"""
 
	def _defaults(self):
		"""
		Add config options.
		"""
		OnlyRPCCRAMTool._defaults(self)
		self.config.disabledServices = []
 
	_types = OnlyRPCCRAMTool._types + (
		('cramSeed', ['xsd:string']),
		('cramToken', ['xsd:string']),
		('listResult', ['xsd:anyType']),
		('argTuple', ['xsd:string']),
		('argList', ['xsd:argTuple']),
	)
 
	def getChallange(self, *args):
		"""
		This builds a single challenge for the challenge response
		authentication scheme. The typo in the name is for backward
		compatibility with older toolservers that were built by me
		before I knew the correct speling for the word challenge ...
 
		The arguments must be strings that are incorporated into
		the challenge building to provide some extra entropy.
 
		Python code to create the token interactively:
 
		>>> import hashlib
		>>> ctx = hashlib.sha1()
		>>> ctx.update(challenge)
		>>> ctx.update(secret)
		>>> print ctx.hexdigest()
 
 
		"""
		return self._getChallenge(*args)
 
	getChallange_signature = ('xsd:string', 'typens:cramSeed')
 
	def getChallange_validate_RPC(self, *args):
		return 0
 
	def getChallange_pre_condition(self, *args):
		assert type(args) in (type(()), type([])), 'The arguments must be a list'
		for el in args:
			assert type(el) in (type(''), type(u'')), 'The arguments must be a list of strings'
 
	def getChallange_post_condition(self, result):
		assert type(result) in (type(''), type(u'')), 'result must be a string'
 
	###########################################################
	# these are methods for the actual servermanager
	###########################################################
 
	def gethostname(self, token):
		"""
		This method returns the hostname of this machine.
		"""
		return [self._createToken(),
			readProcFileFirstLine('/proc/sys/kernel/hostname')
		]
 
	gethostname_signature = ('typens:listResult', 'typens:cramToken')
 
	def getosrelease(self, token):
		"""
		This method returns the release of the kernel of this machine.
		"""
		return [self._createToken(),
			readProcFileFirstLine('/proc/sys/kernel/osrelease')
		]
 
	getosrelease_signature = ('typens:listResult', 'typens:cramToken')
 
	def loadavg(self, token):
		"""
		This returns the loadaverage on 1, 5 and 15 minutes, the
		number of processes and the maximum used PID.
		"""
		file = open('/proc/loadavg')
		(load1, load5, load15, processes, maxpid) = file.readline().split()
		file.close()
		return [self._createToken(),
			atof(load1), atof(load5), atof(load15),
			processes, atoi(maxpid)
		]
 
	loadavg_signature = ('typens:listResult', 'typens:cramToken')
 
	def uptime(self, token):
		"""
		This returns the uptime of the server as two values.
		"""
		file = open('/proc/uptime')
		(uptime1, uptime2) = file.readline().split()
		file.close()
		return [self._createToken(),
			atof(uptime1), atof(uptime2)
		]
 
	uptime_signature = ('typens:listResult', 'typens:cramToken')
 
	def getservices(self, token):
		"""
		This call returns a list of services that can be controlled
		by the servermanager.
		"""
		services = os.listdir('/usr/share/servermanager/service.d')
		list = []
		badlist = self.config.disabledServices
		for srv in services:
			if srv not in badlist:
				list.append(srv)
		return [createToken(), list]
 
	getservices_signature = ('typens:listResult', 'typens:cramToken')
 
	def manageservice(self, token, service, action):
		"""
		This call returns a return code and an error message
		for running a service control script.
		"""
		services = os.listdir('/usr/share/servermanager/service.d')
		if service in services:
			if service not in self.config.disabledServices:
				cmd = os.popen('/usr/share/servermanager/service.d/%s %s' % (service, action), 'r')
				rc = 800
				msg = 'no response from control script'
				for line in cmd.readlines():
					(rc, msg) = line.split(': ')
				return [self._createToken(),
					int(rc), msg.strip()
				]
		return [self._createToken(),
			[900, 'invalid service %s' % service]
		]
 
	manageservice_signature = ('typens:listResult', 'typens:cramToken', 'xsd:string', 'xsd:string')
 
	def getprocesses(self, token, onlyrunning=1):
		"""
		This call returns a list of processes. It can be restricted
		to only running processes. The informations returned for
		each process are: Name, User, State, CPU, MEM and Pids for
		this process group (grouped by user+command)
		"""
		what = (onlyrunning and 'axr') or 'ax'
		pipe = os.popen('ps -o pid,stat,user,pcpu,pmem,comm --no-headers %s' % what)
		lines = pipe.readlines()
		pipe.close()
		proc = {}
		for line in lines:
			(pid, stat, user, pcpu, pmem, comm) = line.split(None, 5)
			pcpu = float(pcpu)
			pmem = float(pmem)
			pid = int(pid)
			comm = comm.strip()
			key = user + ':' + comm
			if proc.has_key(key):
				proc[key]['Pids'].append(pid)
				proc[key]['CPU'] += pcpu
				if pmem > proc[key]['MEM']:
					proc[key]['MEM'] = pmem
			else:
				proc[key] = {
					'Name':comm,
					'User':user,
					'State':stat,
					'CPU':pcpu,
					'MEM':pmem,
					'Pids':[pid]
				}
		list = []
		for k in proc.keys():
			if not(onlyrunning) or proc[k]['CPU'] > 0.0:
				list.append(proc[k])
		list.sort(sortProcTable)
		return [self._createToken(), list]
 
	getprocesses_signature = ('typens:listResult', 'typens:cramToken', 'xsd:int')
 
	def manageprocess(self, token, service, action, parm):
		"""
		This call can kill processes in various ways.
		"""
		if action == 'slay' and parm != 'root':
			os.system("slay '%s'" % parm)
		elif action == 'kill' and int(parm) >= 100:
			os.system("kill %d" % int(parm))
		elif action == 'term' and int(parm) >= 100:
			os.system("kill -9 %d" % int(parm))
		return [self._createToken(), [100, 'action done']]
 
	manageprocess_signature = ('typens:listResult', 'typens:cramToken', 'xsd:string', 'xsd:string')
 
	def manageprocess_pre_condition(self, token, service, action, parm):
		assert action in ('slay', 'kill', 'term'), 'Action must be slay/kill/term'
 
	def getdevstats(self, token):
		"""
		This call returns a l ist of all mounted filesystems with
		usage stats for each
		"""
		pipe = os.popen('df -T -m -P')
		lines = pipe.readlines()
		pipe.close()
		list = []
		for line in lines:
			parts = line.split()
			list.append({
				'Dev':parts[0],
				'Type':parts[1],
				'Total':parts[2],
				'Used':parts[3],
				'Free':parts[4],
				'Mountpoint':parts[6]
			})
		return [self._createToken(), list]
 
	getdevstats_signature = ('typens:listResult', 'typens:cramToken')
 
	def getjobstatus(self, token, jobid):
		"""
		get the status of a registered job
		"""
		return [self._createToken(), getJobStatus(id=jobid)]
 
	getjobstatus_signature = ('typens:listResult', 'typens:cramToken', 'xsd:string')
 
	def getjoboutput(self, token, jobid):
		"""
		Get the stdout, stderr and runtime for a job
		"""
		return [self._createToken(), getJobOutput(id=jobid)]
 
	getjoboutput_signature = ('typens:listResult', 'typens:cramToken', 'xsd:string')
 
	def getjobs(self, token, jobid=None):
		"""
		Get a list of installed jobs
		"""
		list = []
		for job in getJobs():
			if not(jobid) or jobid == job.id:
				sig = job.signature()
				stat = getJobStatus(sig=sig)
				ob = {
					'id':job.id,
					'status':stat,
					'sig':sig,
					'name':job.name
				}
				if jobid:
					ob['desc'] = job.description
					ob['parms'] = job.parms
				list.append(ob)
		return[self._createToken(), list]
 
	getjobs_signature = ('typens:listResult', 'typens:cramToken', 'xsd:anyType')
 
	def startjob(self, token, jobid, parms={}):
		"""
		Starts a given job in the background and returns the number of
		started jobs
		"""
		count = 0
		for jkob in getJobs():
			if job.id == jobid and getJobStatus(sig=job.signature()) != 'running':
				sig = job.signature()
				self._async(startJob, job, parms)
				count += 1
		return [self._createToken(), count]
 
	startjob_signature = ('typens:listResult', 'typens:cramToken', 'xsd:string', 'xsd:anyType')
 
	def startsyncjob(self, token, jobid, parms={}):
		"""
		Starts a given job synchronously and return outpout directly
		"""
		for jkob in getJobs():
			if job.id == jobid and getJobStatus(sig=job.signature()) != 'running':
				sig = job.signature()
				startJob(job, parms)
		return [self._createToken(), getJobOutput(id=jobid)]
 
	startsyncjob_signature = ('typens:listResult', 'typens:cramToken', 'xsd:string', 'xsd:anyType')
 
	def getcountervalues(self, token, counter, parms=[]):
		"""
		Get values for a given counter script.
		"""
		counters = os.listdir('/usr/share/servermanager/counter.d')
		if counter in counters:
			parmstr = ' '.join(map(lambda k: '%s=%s' % (k[0], k[1]), parms))
			cmd = os.popen('/usr/share/servermanager/counter.d/%s values %s' % (counter, parmstr), 'r')
			output = cmd.read()
			(head, body) = output.split('\n\n')
			headerlist = head.split('\n')
			header = []
			for h in headerlist:
				if h:
					header.append(h.split('='))
			valueslist = body.split('\n')
			values = []
			for v in valueslist:
				if v:
					values.append(v.split('='))
			return [self._createToken(), (time.time(), header, values)]
		return [self._createToken(), None]
 
	getcountervalues_signature = ('typens:listResult', 'typens:cramToken', 'xsd:string', 'typens:argList')
 
	def getcountervariables(self, token, counter):
		"""
		Get variables for a given counter script.
		"""
		counters = os.listdir('/usr/share/servermanager/counter.d')
		if counter in counters:
			cmd = os.popen('/usr/share/servermanager/counter.d/%s variables' % counter, 'r')
			output = cmd.read()
			headerlist = output.split('\n')
			header = []
			for h in headerlist:
				if h:
					header.append(h)
			return [self._createToken(), header]
		return [self._createToken(), None]
 
	getcountervariables_signature = ('typens:listResult', 'typens:cramToken', 'xsd:string')
 
registerTool(ServerManagerTool, 'servermanager')