Merge branch 'v2/master'
Conflicts:
doc/rteval.8
rteval.spec
rteval/cyclictest.py
rteval/hackbench.py
rteval/rteval.conf
rteval/rteval.py
setup.py
Signed-off-by: Clark Williams <williams@redhat.com>
diff --git a/doc/rteval.8 b/doc/rteval.8
index e1bb269..fa24509 100644
--- a/doc/rteval.8
+++ b/doc/rteval.8
@@ -63,12 +63,12 @@
.B \-v, \-\-verbose
Increase the verbosity of output during the test run
.TP
-.B \-w, \-\-workdir=WORKDIR
+.B \-w WORKDIR, \-\-workdir=WORKDIR
Tell rteval to change directory to WORKDIR before creating any
subdirectories for report files. The default WORKDIR is the directory
in which rteval was started.
.TP
-.B \-l, \-\-loaddir=LOADDIR
+.B \-l LOADDIR, \-\-loaddir=LOADDIR
Tell rteval where to find the source for the loads
.TP
.B \-\-loads\-cpulist=CPULIST
@@ -84,12 +84,51 @@
.B \-D, \-\-debug
Turn on debugging prints during run
.TP
-.B \-X, \-\-xmprpc-submit=HOST
+.B \-X HOST, \-\-xmprpc-submit=HOST
Have rteval send report data to HOST following the run, using XML-RPC
.TP
+.B \-P, \-\-xmlrpc-no-abort
+Do not abort if XML-RPC server do not respond to ping request
+.TP
.B \-Z, \-\-summarize
Have rteval summarize an existing report. This will not cause loads or
meausurement utilities to be run.
+.TP
+.B \-H, \-\-raw-histogram
+Generate raw histogram data for an already existing XML report
+.TP
+.B \-f INIFILE, \-\-inifile=INIFILE
+Initialization file for configuring loads and behavior
+.TP
+.B \-a COMMENT, \-\-annotate=COMMENT
+Add a little annotation which is stored in the report
+.TP
+.B \-L, \-\-logging
+Log the output of the loads in the report directory
+.TP
+.B \-O, \-\-onlyload
+Only run the loads (don't run measurement threads)
+
+.SH MODULE OPTIONS
+These are options that affect the execution behavior of the measurement and load modules.
+.TP
+.B \-\-cyclictest-priority=PRIORITY
+SCHED_FIFO priority for measurement threads (default: 95)
+.TP
+.B \-\-cyclictest-interval=INTERVAL
+Measurement thread interval in microseconds (default: 100)
+.TP
+.B \-\-cyclictest-distance=DISTANCE
+Interval increment in microseconds (default: 0)
+.TP
+.B \-\-cyclictest-buckets=NBUCKETS
+Number of 1 microsecond histogram buckets (default: 2000)
+.TP
+.B \-\-hackbench-jobspercore=N
+Number of jobs per online-core for hackbench load
+.TP
+.B \-\-kcompile-jobspercore=N
+Number of jobs per online-core for kernel compile load
.\" .SH SEE ALSO
.\" .BR bar (1),
.\" .BR baz (1).
diff --git a/rteval.spec b/rteval.spec
index a41a342..d9f7d40 100644
--- a/rteval.spec
+++ b/rteval.spec
@@ -261,7 +261,7 @@
in a dictionary rather than as discrete parameters
- added logging for load output
- * Tue Apr 13 2010 Clark Williams <williams@redhat.com> - 1.21-1
+* Tue Apr 13 2010 Clark Williams <williams@redhat.com> - 1.21-1
- from Luis Claudio Goncalves <lgoncalv@redhat.com>:
- remove unecessary wait() call in cyclictest.py
- close /dev/null after using it
@@ -274,7 +274,7 @@
- added --annotate feature to rteval
- updates to xmlrpc code
- * Tue Apr 6 2010 Clark Williams <williams@redhat.com> - 1.20-1
+* Tue Apr 6 2010 Clark Williams <williams@redhat.com> - 1.20-1
- code fixes from Luis Claudio Goncalves <lgoncalv@redhat.com>
- from David Sommerseth <davids@redhat.com>:
- xmlrpc server updates
diff --git a/rteval/cyclictest.py b/rteval/cyclictest.py
new file mode 100644
index 0000000..fca5c6a
--- /dev/null
+++ b/rteval/cyclictest.py
@@ -0,0 +1,261 @@
+#
+# cyclictest.py - object to manage a cyclictest executable instance
+#
+# Copyright 2009,2010 Clark Williams <williams@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# For the avoidance of doubt the "preferred form" of this code is one which
+# is in an open unpatent encumbered format. Where cryptographic key signing
+# forms part of the process of creating an executable the information
+# including keys needed to generate an equivalently functional executable
+# are deemed to be part of the source code.
+#
+
+import os
+import sys
+import subprocess
+import tempfile
+import time
+import signal
+import schedutils
+from threading import *
+import libxml2
+import xmlout
+
+class RunData(object):
+ '''class to keep instance data from a cyclictest run'''
+ def __init__(self, id, type, priority):
+ self.id = id
+ self.type = type
+ self.priority = int(priority)
+ self.description = ''
+ # histogram of data
+ self.samples = {}
+ self.numsamples = 0
+ self.min = 100000000
+ self.max = 0
+ self.stddev = 0.0
+ self.mean = 0.0
+ self.mode = 0.0
+ self.median = 0.0
+ self.range = 0.0
+ self.mad = 0.0
+ self.variance = 0.0
+
+ def sample(self, value):
+ self.samples[value] += self.samples.setdefault(value, 0) + 1
+ if value > self.max: self.max = value
+ if value < self.min: self.min = value
+ self.numsamples += 1
+
+ def bucket(self, index, value):
+ self.samples[index] = self.samples.setdefault(index, 0) + value
+ if value and index > self.max: self.max = index
+ if value and index < self.min: self.min = index
+ self.numsamples += value
+
+ def reduce(self):
+ import math
+
+ # check to see if we have any samples and if we
+ # only have 1 (or none) set the calculated values
+ # to zero and return
+ if self.numsamples <= 1:
+ print "skipping %s (%d samples)" % (self.id, self.numsamples)
+ self.variance = 0
+ self.mad = 0
+ self.stddev = 0
+ return
+
+ print "reducing %s" % self.id
+ total = 0
+ keys = self.samples.keys()
+ keys.sort()
+ sorted = []
+
+ mid = self.numsamples / 2
+
+ # mean, mode, and median
+ occurances = 0
+ lastkey = -1
+ for i in keys:
+ if mid > total and mid <= (total + self.samples[i]):
+ if self.numsamples & 1 and mid == total+1:
+ self.median = (lastkey + i) / 2
+ else:
+ self.median = i
+ total += (i * self.samples[i])
+ if self.samples[i] > occurances:
+ occurances = self.samples[i]
+ self.mode = i
+ self.mean = float(total) / float(self.numsamples)
+
+ # range
+ for i in keys:
+ if self.samples[i]:
+ low = i
+ break
+ high = keys[-1]
+ while high and self.samples[high] == 0:
+ high -= 1
+ self.range = high - low
+
+ # Mean Absolute Deviation and Variance
+ madsum = 0
+ varsum = 0
+ for i in keys:
+ madsum += float(abs(float(i) - self.mean) * self.samples[i])
+ varsum += float(((float(i) - self.mean) ** 2) * self.samples[i])
+ self.mad = madsum / self.numsamples
+ self.variance = varsum / (self.numsamples - 1)
+
+ # standard deviation
+ self.stddev = math.sqrt(self.variance)
+
+ def genxml(self, x):
+ if self.type == 'system':
+ x.openblock(self.type, {'description':self.description})
+ else:
+ x.openblock(self.type, {'id': self.id, 'priority': self.priority})
+ x.openblock('statistics')
+ x.taggedvalue('samples', str(self.numsamples))
+ x.taggedvalue('minimum', str(self.min), {"unit": "us"})
+ x.taggedvalue('maximum', str(self.max), {"unit": "us"})
+ x.taggedvalue('median', str(self.median), {"unit": "us"})
+ x.taggedvalue('mode', str(self.mode), {"unit": "us"})
+ x.taggedvalue('range', str(self.range), {"unit": "us"})
+ x.taggedvalue('mean', str(self.mean), {"unit": "us"})
+ x.taggedvalue('mean_absolute_deviation', str(self.mad), {"unit": "us"})
+ x.taggedvalue('variance', str(self.variance), {"unit": "us"})
+ x.taggedvalue('standard_deviation', str(self.stddev), {"unit": "us"})
+ x.closeblock()
+ h = libxml2.newNode('histogram')
+ h.newProp('nbuckets', str(len(self.samples)))
+ keys = self.samples.keys()
+ keys.sort()
+ for k in keys:
+ b = libxml2.newNode('bucket')
+ b.newProp('index', str(k))
+ b.newProp('value', str(self.samples[k]))
+ h.addChild(b)
+ x.AppendXMLnodes(h)
+ x.closeblock()
+
+
+class Cyclictest(Thread):
+ def __init__(self, params={}):
+ Thread.__init__(self)
+ self.duration = params.setdefault('duration', None)
+ self.keepdata = params.setdefault('keepdata', False)
+ self.stopevent = Event()
+ self.finished = Event()
+ self.threads = params.setdefault('threads', None)
+ self.priority = int(params.setdefault('priority', 95))
+ self.interval = int(params.setdefault('interval', 100))
+ self.distance = int(params.setdefault('distance', 0))
+ self.buckets = int(params.setdefault('buckets', 2000))
+ self.debugging = params.setdefault('debugging', False)
+ self.reportfile = 'cyclictest.rpt'
+ self.params = params
+ f = open('/proc/cpuinfo')
+ self.data = {}
+ numcores = 0
+ for line in f:
+ if line.startswith('processor'):
+ core = line.split()[-1]
+ self.data[core] = RunData(core, 'core', self.priority)
+ numcores += 1
+ if line.startswith('model name'):
+ desc = line.split(': ')[-1][:-1]
+ self.data[core].description = ' '.join(desc.split())
+ f.close()
+ self.numcores = numcores
+ self.data['system'] = RunData('system', 'system', self.priority)
+ self.data['system'].description = ("(%d cores) " % numcores) + self.data['0'].description
+ self.dataitems = len(self.data.keys())
+ self.debug("system has %d cpu cores" % (self.dataitems - 1))
+ self.numanodes = params.setdefault('numanodes', 0)
+
+ def __del__(self):
+ pass
+
+ def debug(self, str):
+ if self.debugging: print "cyclictest: %s" % str
+
+ def getmode(self):
+ if self.numanodes > 1:
+ self.debug("running in NUMA mode (%d nodes)" % self.numanodes)
+ return '--numa'
+ self.debug("running in SMP mode")
+ return '--smp'
+
+ def run(self):
+
+ self.cmd = ['cyclictest',
+ '-qm',
+ '-i %d' % self.interval,
+ '-d %d' % self.distance,
+ '-h %d' % self.buckets,
+ "-p %d" % self.priority,
+ self.getmode(),
+ ]
+
+ if self.threads:
+ self.cmd.append("-t%d" % int(self.threads))
+
+ self.debug("starting with cmd: %s" % " ".join(self.cmd))
+ null = os.open('/dev/null', os.O_RDWR)
+ c = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=null, stdin=null)
+ while True:
+ if self.stopevent.isSet():
+ break
+ if c.poll():
+ self.debug("process died! bailng out...")
+ break
+ time.sleep(1.0)
+ self.debug("stopping")
+ if c.poll() == None:
+ os.kill(c.pid, signal.SIGINT)
+ # now parse the histogram output
+ for line in c.stdout:
+ line = line.strip()
+ if line.startswith('#') or len(line) == 0: continue
+ vals = line.split()
+ if len(vals) == 0: continue
+ index = int(vals[0])
+ for i in range(0, len(self.data)-1):
+ if str(i) not in self.data: continue
+ self.data[str(i)].bucket(index, int(vals[i+1]))
+ self.data['system'].bucket(index, int(vals[i+1]))
+ for n in self.data.keys():
+ self.data[n].reduce()
+ self.finished.set()
+ os.close(null)
+
+ def genxml(self, x):
+ x.openblock('cyclictest')
+ x.taggedvalue('command_line', " ".join(self.cmd))
+
+ self.data["system"].genxml(x)
+ for t in range(0, self.numcores):
+ if str(t) not in self.data: continue
+ self.data[str(t)].genxml(x)
+ x.closeblock()
+
+
+if __name__ == '__main__':
+ c = CyclicTest()
+ c.run()
diff --git a/rteval/hackbench.py b/rteval/hackbench.py
new file mode 100644
index 0000000..08a0e37
--- /dev/null
+++ b/rteval/hackbench.py
@@ -0,0 +1,160 @@
+#
+# hackbench.py - class to manage an instance of hackbench load
+#
+# Copyright 2009,2010 Clark Williams <williams@redhat.com>
+# Copyright 2009,2010 David Sommerseth <davids@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# For the avoidance of doubt the "preferred form" of this code is one which
+# is in an open unpatent encumbered format. Where cryptographic key signing
+# forms part of the process of creating an executable the information
+# including keys needed to generate an equivalently functional executable
+# are deemed to be part of the source code.
+#
+
+import sys
+import os
+import time
+import glob
+import subprocess
+import errno
+from signal import SIGTERM
+from signal import SIGKILL
+sys.pathconf = "."
+import load
+
+class Hackbench(load.Load):
+ def __init__(self, params={}):
+ load.Load.__init__(self, "hackbench", params)
+
+ def __del__(self):
+ null = open("/dev/null", "w")
+ subprocess.call(['killall', '-9', 'hackbench'],
+ stdout=null, stderr=null)
+ os.close(null)
+
+ def setup(self):
+ # figure out how many nodes we have
+ self.nodes = [ n.split('/')[-1][4:] for n in glob.glob('/sys/devices/system/node/node*') ]
+
+ # get the cpus for each node
+ self.cpus = {}
+ biggest = 0
+ for n in self.nodes:
+ self.cpus[n] = [ int(c.split('/')[-1][3:]) for c in glob.glob('/sys/devices/system/node/node%s/cpu[0-9]*' % n) ]
+ self.cpus[n].sort()
+ if len(self.cpus[n]) > biggest:
+ biggest = len(self.cpus[n])
+
+ # setup jobs based on the number of cores available per node
+ self.jobs = biggest * 3
+
+ # figure out if we can use numactl or have to use taskset
+ self.__usenumactl = False
+ self.__multinodes = False
+ if len(self.nodes) > 1:
+ self.__multinodes = True
+ print "hackbench: running with multiple nodes (%d)" % len(self.nodes)
+ if os.path.exists('/usr/bin/numactl'):
+ self.__usenumactl = True
+ print "hackbench: using numactl for thread affinity"
+
+ self.args = ['hackbench', '-P',
+ '-g', str(self.jobs),
+ '-l', str(self.params.setdefault('loops', '1000')),
+ '-s', str(self.params.setdefault('datasize', '1000'))
+ ]
+ self.err_sleep = 5.0
+ self.tasks = {}
+
+ def build(self):
+ self.ready = True
+
+ def __starton(self, node):
+ if self.__multinodes:
+ if self.__usenumactl:
+ args = [ 'numactl', '--cpunodebind', node ] + self.args
+ else:
+ cpulist = ",".join([ str(n) for n in self.cpus[node] ])
+ args = ['taskset', '-c', cpulist ] + self.args
+ else:
+ args = self.args
+
+ self.debug("hackbench starting: %s" % " ".join(args))
+
+ return subprocess.Popen(args,
+ stdin=self.null,
+ stdout=self.out,
+ stderr=self.err)
+
+ def runload(self):
+ # if we don't have any jobs just wait for the stop event and return
+ if self.jobs == 0:
+ self.stopevent.wait()
+ return
+ self.null = os.open("/dev/null", os.O_RDWR)
+ if self.logging:
+ self.out = self.open_logfile("hackbench.stdout")
+ self.err = self.open_logfile("hackbench.stderr")
+ else:
+ self.out = self.err = self.null
+ self.debug("starting loop (jobs: %d)" % self.jobs)
+
+ for n in self.nodes:
+ self.tasks[n] = self.__starton(n)
+
+ while not self.stopevent.isSet():
+ for n in self.nodes:
+ try:
+ # if poll() returns an exit status, restart
+ if self.tasks[n].poll() != None:
+ self.tasks[n].wait()
+ self.tasks[n] = self.__starton(n)
+ except OSError, e:
+ if e.errno != errno.ENOMEM:
+ raise
+ # Catch out-of-memory errors and wait a bit to (hopefully)
+ # ease memory pressure
+ print "hackbench: %s, sleeping for %f seconds" % (e.strerror, self.err_sleep)
+ time.sleep(self.err_sleep)
+ if self.err_sleep < 60.0:
+ self.err_sleep *= 2.0
+ if self.err_sleep > 60.0:
+ self.err_sleep = 60.0
+
+ self.debug("stopping")
+ for n in self.nodes:
+ if self.tasks[n].poll() == None:
+ os.kill(self.tasks[n].pid, SIGKILL)
+ self.tasks[n].wait()
+
+ self.debug("returning from runload()")
+ os.close(self.null)
+ if self.logging:
+ os.close(self.out)
+ os.close(self.err)
+
+ def genxml(self, x):
+ x.taggedvalue('command_line', self.jobs and ' '.join(self.args) or None,
+ {'name':'hackbench', 'run': self.jobs and '1' or '0'})
+
+def create(params = {}):
+ return Hackbench(params)
+
+
+if __name__ == '__main__':
+ h = Hackbench(params={'debugging':True, 'verbose':True})
+ h.run()
diff --git a/rteval/rteval.conf b/rteval/rteval.conf
new file mode 100644
index 0000000..0d1afb3
--- /dev/null
+++ b/rteval/rteval.conf
@@ -0,0 +1,31 @@
+[rteval]
+verbose: False
+keepdata: True
+debugging: False
+duration: 60.0
+report_interval: 600
+
+[cyclictest]
+buckets: 2000
+interval: 100
+distance: 0
+priority: 95
+
+[loads]
+kcompile: module
+hackbench: module
+dbench: external
+
+[kcompile]
+source: linux-2.6.39.tar.bz2
+jobspercore: 2
+
+[hackbench]
+jobspercore: 2
+
+[dbench]
+source: dbench.tar.gz
+setup: tar -xvf dbench.tar.gz
+build: ./configure && make
+runload: dbench -c ./client.txt 10
+
diff --git a/rteval/rteval.py b/rteval/rteval.py
new file mode 100644
index 0000000..0d1b02f
--- /dev/null
+++ b/rteval/rteval.py
@@ -0,0 +1,1035 @@
+#!/usr/bin/python -tt
+#
+# rteval - script for evaluating platform suitability for RT Linux
+#
+# This program is used to determine the suitability of
+# a system for use in a Real Time Linux environment.
+# It starts up various system loads and measures event
+# latency while the loads are running. A report is generated
+# to show the latencies encountered during the run.
+#
+# Copyright 2009,2010,2011,2012 Clark Williams <williams@redhat.com>
+# Copyright 2009,2010,2011,2012 David Sommerseth <davids@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+#
+# For the avoidance of doubt the "preferred form" of this code is one which
+# is in an open unpatent encumbered format. Where cryptographic key signing
+# forms part of the process of creating an executable the information
+# including keys needed to generate an equivalently functional executable
+# are deemed to be part of the source code.
+#
+
+import sys
+import os
+import os.path
+import time
+import string
+import threading
+import subprocess
+import socket
+import optparse
+import tempfile
+import statvfs
+import shutil
+import signal
+import rtevalclient
+import ethtool
+import xmlrpclib
+import platform
+import fnmatch
+import glob
+from datetime import datetime
+from distutils import sysconfig
+
+# put local path at start of list to overide installed methods
+sys.path.insert(0, "./rteval")
+import util
+import load
+import cyclictest
+import xmlout
+import dmi
+import rtevalConfig
+import rtevalMailer
+from cputopology import CPUtopology
+
+
+pathSave={}
+def getcmdpath(which):
+ """
+ getcmdpath is a method which allows finding an executable in the PATH
+ directories to call it from full path
+ """
+ if not pathSave.has_key(which):
+ for path in os.environ['PATH'].split(':'):
+ cmdfile = os.path.join(path, which)
+ if os.path.isfile(cmdfile) and os.access(cmdfile, os.X_OK):
+ pathSave[which] = cmdfile
+ break
+ if not pathSave[which]:
+ raise RuntimeError, "Command '%s' is unknown on this system" % which
+ return pathSave[which]
+
+
+sigint_received = False
+def sigint_handler(signum, frame):
+ global sigint_received
+ sigint_received = True
+ print "*** SIGINT received - stopping rteval run ***"
+
+def sigterm_handler(signum, frame):
+ raise RuntimeError, "SIGTERM received!"
+
+class RtEval(object):
+ def __init__(self, cmdargs):
+ self.version = "1.41"
+ self.load_modules = []
+ self.workdir = os.getcwd()
+ self.reportdir = os.getcwd()
+ self.inifile = None
+ self.cmd_options = {}
+ self.start = datetime.now()
+ self.init = 'unknown'
+
+ default_config = {
+ 'rteval': {
+ 'verbose' : False,
+ 'keepdata' : True,
+ 'debugging' : False,
+ 'duration' : '60',
+ 'sysreport' : False,
+ 'reportdir' : None,
+ 'reportfile' : None,
+ 'installdir' : '/usr/share/rteval',
+ 'srcdir' : '/usr/share/rteval/loadsource',
+ 'xmlrpc' : None,
+ 'xslt_report': '/usr/share/rteval/rteval_text.xsl',
+ 'report_interval': '600',
+ 'logging' : False,
+ },
+ 'loads' : {
+ 'kcompile' : 'module',
+ 'hackbench' : 'module',
+ },
+ 'kcompile' : {
+ 'source' : 'linux-2.6.39.tar.bz2',
+ 'jobspercore': '2',
+ },
+ 'hackbench' : {
+ 'source' : 'hackbench.tar.bz2',
+ 'jobspercore': '5',
+ },
+ 'cyclictest' : {
+ 'interval' : '100',
+ 'buckets' : '2000',
+ }
+ }
+
+ # setup initial configuration
+ self.config = rtevalConfig.rtevalConfig(default_config, logfunc=self.info)
+
+ # parse command line options
+ self.parse_options(cmdargs)
+
+ # read in config file info
+ self.inifile = self.config.Load(self.cmd_options.inifile)
+
+ # copy the command line options into the rteval config section
+ # (cmd line overrides config file values)
+ self.config.AppendConfig('rteval', self.cmd_options)
+
+ if self.cmd_options.cyclictest_interval != None:
+ self.config.AppendConfig('cyclictest', { "interval":self.cmd_options.cyclictest_interval })
+
+ if self.cmd_options.cyclictest_distance != None:
+ self.config.AppendConfig('cyclictest', { "distance":self.cmd_options.cyclictest_distance })
+
+ if self.cmd_options.cyclictest_buckets != None:
+ self.config.AppendConfig('cyclictest', { "buckets":self.cmd_options.cyclictest_distance })
+
+ if self.cmd_options.cyclictest_priority != None:
+ self.config.AppendConfig('cyclictest', { "priority":self.cmd_options.cyclictest_priority })
+
+ if self.cmd_options.hackbench_jobspercore != None:
+ self.config.AppendConfig('hackbench', { "jobspercore":self.cmd_options.hackbench_jobspercore })
+
+ if self.cmd_options.kcompile_jobspercore != None:
+ self.config.AppendConfig('kcompile', { "jobspercore":self.cmd_options.kcompile_jobspercore })
+
+ self.debug("workdir: %s" % self.workdir)
+
+ # prepare a mailer, if that's configured
+ if self.config.HasSection('smtp'):
+ self.mailer = rtevalMailer.rtevalMailer(self.config.GetSection('smtp'))
+ else:
+ self.mailer = None
+
+ self.loads = []
+ self.cputopology = None
+ self.numcores = None
+ self.memsize = None
+ self.current_clocksource = None
+ self.available_clocksource = None
+ self.services = None
+ self.kthreads = None
+ self.xml = None
+ self.baseos = "unknown"
+ self.annotate = self.cmd_options.annotate
+
+ if not self.config.xslt_report.startswith(self.config.installdir):
+ self.config.xslt_report = os.path.join(self.config.installdir, "rteval_text.xsl")
+
+ if not os.path.exists(self.config.xslt_report):
+ raise RuntimeError, "can't find XSL template (%s)!" % self.config.xslt_report
+
+ # Add rteval directory into module search path
+ sys.path.insert(0, '%s/rteval' % sysconfig.get_python_lib())
+
+ # generate a set of "junk" characters to use for filtering later
+ self.junk = ""
+ for c in range(0, 0xff):
+ s = chr(c)
+ if s not in string.printable:
+ self.junk += s
+ self.transtable = string.maketrans("", "")
+
+ # If --xmlrpc-submit is given, check that we can access the server
+ res = None
+ if self.config.xmlrpc:
+ self.debug("Checking if XML-RPC server '%s' is reachable" % self.config.xmlrpc)
+ attempt = 0
+ warning_sent = False
+ ping_failed = False
+ while attempt < 6:
+ try:
+ client = rtevalclient.rtevalclient("http://%s/rteval/API1/" % self.config.xmlrpc)
+ res = client.Hello()
+ attempt = 10
+ ping_failed = False
+ except xmlrpclib.ProtocolError:
+ # Server do not support Hello(), but is reachable
+ self.info("Got XML-RPC connection with %s but it did not support Hello()"
+ % self.config.xmlrpc)
+ res = None
+ except socket.error, err:
+ self.info("Could not establish XML-RPC contact with %s\n%s"
+ % (self.config.xmlrpc, str(err)))
+
+ if (self.mailer is not None) and (not warning_sent):
+ self.mailer.SendMessage("[RTEVAL:WARNING] Failed to ping XML-RPC server",
+ "Server %s did not respond. Not giving up yet."
+ % self.config.xmlrpc)
+ warning_sent = True
+
+ # Do attempts handling
+ attempt += 1
+ if attempt > 5:
+ break # To avoid sleeping before we abort
+
+ print "Failed pinging XML-RPC server. Doing another attempt(%i) " % attempt
+ time.sleep(attempt*15) # Incremental sleep - sleep attempts*15 seconds
+ ping_failed = True
+
+ if ping_failed:
+ if not self.cmd_options.xmlrpc_noabort:
+ print "ERROR: Could not reach XML-RPC server '%s'. Aborting." % self.config.xmlrpc
+ sys.exit(2)
+ else:
+ print "WARNING: Could not ping the XML-RPC server. Will continue anyway."
+
+ if res:
+ self.info("Verified XML-RPC connection with %s (XML-RPC API version: %i)"
+ % (res["server"], res["APIversion"]))
+ self.debug("Recieved greeting: %s" % res["greeting"])
+
+
+ def get_cpu_topology(self):
+ ''' figure out how many processors we have available'''
+
+ topology = CPUtopology()
+ topology.parse()
+
+ self.numcores = topology.getCPUcores(True)
+ self.debug("counted %d cores (%d online) and %d sockets" %
+ (topology.getCPUcores(False), self.numcores,
+ topology.getCPUsockets()))
+ return topology.getXMLdata()
+
+ def __get_services_sysvinit(self):
+ reject = ('functions', 'halt', 'killall', 'single', 'linuxconf', 'kudzu',
+ 'skeleton', 'README', '*.dpkg-dist', '*.dpkg-old', 'rc', 'rcS',
+ 'single', 'reboot', 'bootclean.sh')
+ for sdir in ('/etc/init.d', '/etc/rc.d/init.d'):
+ if os.path.isdir(sdir):
+ servicesdir = sdir
+ break
+ if not servicesdir:
+ raise RuntimeError, "No services dir (init.d) found on your system"
+ self.debug("Services located in %s, going through each service file to check status" % servicesdir)
+ ret_services = {}
+ for service in glob.glob(os.path.join(servicesdir, '*')):
+ servicename = os.path.basename(service)
+ if not [1 for p in reject if fnmatch.fnmatch(servicename, p)] and os.access(service, os.X_OK):
+ cmd = '%s -qs "\(^\|\W\)status)" %s' % (getcmdpath('grep'), service)
+ c = subprocess.Popen(cmd, shell=True)
+ c.wait()
+ if c.returncode == 0:
+ cmd = ['env', '-i', 'LANG="%s"' % os.environ['LANG'], 'PATH="%s"' % os.environ['PATH'], 'TERM="%s"' % os.environ['TERM'], service, 'status']
+ c = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ c.wait()
+ if c.returncode == 0 and (c.stdout.read() or c.stderr.read()):
+ ret_services[servicename] = 'running'
+ else:
+ ret_services[servicename] = 'not running'
+ else:
+ ret_services[servicename] = 'unknown'
+ return ret_services
+
+ def __get_services_systemd(self):
+ ret_services = {}
+ cmd = '%s list-unit-files -t service --no-legend' % getcmdpath('systemctl')
+ self.debug("cmd: %s" % cmd)
+ c = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ for p in c.stdout:
+ # p are lines like "servicename.service status"
+ v = p.strip().split()
+ ret_services[v[0].split('.')[0]] = v[1]
+ return ret_services
+
+ def get_services(self):
+ cmd = [getcmdpath('ps'), '-ocomm=', '1']
+ c = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ self.init = c.stdout.read().strip()
+ if self.init == 'systemd':
+ self.debug("Using systemd to get services status")
+ return self.__get_services_systemd()
+ elif self.init == 'init':
+ self.init = 'sysvinit'
+ self.debug("Using sysvinit to get services status")
+ return self.__get_services_sysvinit()
+ else:
+ raise RuntimeError, "Unknown init system (%s)" % self.init
+ return {}
+
+ def get_kthreads(self):
+ policies = {'FF':'fifo', 'RR':'rrobin', 'TS':'other', '?':'unknown' }
+ ret_kthreads = {}
+ self.debug("getting kthread status")
+ cmd = '%s -eocommand,pid,policy,rtprio,comm' % getcmdpath('ps')
+ self.debug("cmd: %s" % cmd)
+ c = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+ for p in c.stdout:
+ v = p.strip().split()
+ kcmd = v.pop(0)
+ try:
+ if int(v[0]) > 0 and kcmd.startswith('[') and kcmd.endswith(']'):
+ ret_kthreads[v[0]] = {'policy' : policies[v[1]],
+ 'priority' : v[2], 'name' : v[3] }
+ except ValueError:
+ pass # Ignore lines which don't have a number in the first row
+ return ret_kthreads
+
+ def get_modules(self):
+ modlist = []
+ try:
+ fp = open('/proc/modules', 'r')
+ line = fp.readline()
+ while line:
+ mod = line.split()
+ modlist.append({"modname": mod[0],
+ "modsize": mod[1],
+ "numusers": mod[2],
+ "usedby": mod[3],
+ "modstate": mod[4]})
+ line = fp.readline()
+ fp.close()
+ except Exception, err:
+ raise err
+ return modlist
+
+ def parse_options(self, cmdargs):
+ '''parse the command line arguments'''
+ parser = optparse.OptionParser()
+ parser.add_option("-d", "--duration", dest="duration",
+ type="string", default=self.config.duration,
+ help="specify length of test run (default: %default)")
+ parser.add_option("-v", "--verbose", dest="verbose",
+ action="store_true", default=self.config.verbose,
+ help="turn on verbose prints (default: %default)")
+ parser.add_option("-w", "--workdir", dest="workdir",
+ type="string", default=self.workdir,
+ help="top directory for rteval data (default: %default)")
+ parser.add_option("-l", "--loaddir", dest="srcdir",
+ type="string", default=self.config.srcdir,
+ help="directory for load source tarballs (default: %default)")
+ parser.add_option("-i", "--installdir", dest="installdir",
+ type="string", default=self.config.installdir,
+ help="place to locate installed templates (default: %default)")
+ parser.add_option("-s", "--sysreport", dest="sysreport",
+ action="store_true", default=self.config.sysreport,
+ help='run sysreport to collect system data (default: %default)')
+ parser.add_option("-D", '--debug', dest='debugging',
+ action='store_true', default=self.config.debugging,
+ help='turn on debug prints (default: %default)')
+ parser.add_option("-X", '--xmlrpc-submit', dest='xmlrpc',
+ action='store', default=self.config.xmlrpc, metavar='HOST',
+ help='Hostname to XML-RPC server to submit reports')
+ parser.add_option("-P", "--xmlrpc-no-abort", dest="xmlrpc_noabort",
+ action='store_true', default=False,
+ help="Do not abort if XML-RPC server do not respond to ping request");
+ parser.add_option("-Z", '--summarize', dest='summarize',
+ action='store_true', default=False,
+ help='summarize an already existing XML report')
+ parser.add_option("-H", '--raw-histogram', dest='rawhistogram',
+ action='store_true', default=False,
+ help='Generate raw histogram data for an already existing XML report')
+ parser.add_option("-f", "--inifile", dest="inifile",
+ type='string', default=None,
+ help="initialization file for configuring loads and behavior")
+ parser.add_option("-a", "--annotate", dest="annotate",
+ type="string", default=None,
+ help="Add a little annotation which is stored in the report")
+ parser.add_option("-L", "--logging", dest="logging",
+ action='store_true', default=False,
+ help='log the output of the loads in the report directory')
+ parser.add_option("-O", "--onlyload", dest="onlyload",
+ action='store_true', default=False,
+ help="only run the loads (don't run measurement threads)")
+
+ # module options
+ parser.add_option("", "--cyclictest-interval", dest="cyclictest_interval",
+ action="store", type="int",
+ help="cyclictest measurement interval in microseconds")
+ parser.add_option("", "--cyclictest-distance", dest="cyclictest_distance",
+ action="store", type="int",
+ help="cyclictest measurement interval increment in microseconds")
+ parser.add_option("", "--cyclictest-buckets", dest="cyclictest_buckets",
+ action="store", type="int",
+ help="number of cyclictest 1 microsecond histogram buckets")
+ parser.add_option("", "--cyclictest-priority", dest="cyclictest_priority",
+ action="store", type="int",
+ help="SCHED_FIFO priority of measurement threads")
+
+ parser.add_option("", "--hackbench-jobspercore", dest="hackbench_jobspercore",
+ action="store", type="int",
+ help="number of hackbench jobs per-core")
+ parser.add_option("", "--kcompile-jobspercore", dest="kcompile_jobspercore",
+ action="store", type="int",
+ help="number of kernel compile jobs per-core")
+
+
+ (self.cmd_options, self.cmd_arguments) = parser.parse_args(args = cmdargs)
+ if self.cmd_options.duration:
+ mult = 1.0
+ v = self.cmd_options.duration.lower()
+ if v.endswith('s'):
+ v = v[:-1]
+ elif v.endswith('m'):
+ v = v[:-1]
+ mult = 60.0
+ elif v.endswith('h'):
+ v = v[:-1]
+ mult = 3600.0
+ elif v.endswith('d'):
+ v = v[:-1]
+ mult = 3600.0 * 24.0
+ self.cmd_options.duration = float(v) * mult
+ self.workdir = os.path.abspath(self.cmd_options.workdir)
+
+
+ def debug(self, str):
+ if self.config.debugging is True:
+ print "rteval: %s" % str
+
+ def info(self, str):
+ if self.config.verbose is True:
+ print str
+
+ def run_sysreport(self):
+ import glob
+ if os.path.exists('/usr/sbin/sosreport'):
+ exe = '/usr/sbin/sosreport'
+ elif os.path.exists('/usr/sbin/sysreport'):
+ exe = '/usr/sbin/sysreport'
+ else:
+ raise RuntimeError, "Can't find sosreport/sysreport"
+
+ self.debug("report tool: %s" % exe)
+ options = ['-k', 'rpm.rpmva=off',
+ '--name=rteval',
+ '--batch',
+ '--no-progressbar']
+
+ self.info("Generating SOS report")
+ self.info("using command %s" % " ".join([exe]+options))
+ subprocess.call([exe] + options)
+ for s in glob.glob('/tmp/s?sreport-rteval-*'):
+ self.debug("moving %s to %s" % (s, self.reportdir))
+ shutil.move(s, self.reportdir)
+
+
+ def genxml(self, duration, accum, samples, xslt = None):
+ seconds = duration.seconds
+ hours = seconds / 3600
+ if hours: seconds -= (hours * 3600)
+ minutes = seconds / 60
+ if minutes: seconds -= (minutes * 60)
+ (sys, node, release, ver, machine) = os.uname()
+
+ # Start new XML report
+ self.xmlreport = xmlout.XMLOut('rteval', self.version)
+ self.xmlreport.NewReport()
+
+ self.xmlreport.openblock('run_info', {'days': duration.days,
+ 'hours': hours,
+ 'minutes': minutes,
+ 'seconds': seconds})
+ self.xmlreport.taggedvalue('date', self.start.strftime('%Y-%m-%d'))
+ self.xmlreport.taggedvalue('time', self.start.strftime('%H:%M:%S'))
+ if self.annotate:
+ self.xmlreport.taggedvalue('annotate', self.annotate)
+ self.xmlreport.closeblock()
+ self.xmlreport.openblock('uname')
+ self.xmlreport.taggedvalue('node', node)
+ isrt = 1
+ if ver.find(' RT ') == -1:
+ isrt = 0
+ self.xmlreport.taggedvalue('kernel', release, {'is_RT':isrt})
+ self.xmlreport.taggedvalue('arch', machine)
+ self.xmlreport.taggedvalue('baseos', self.baseos)
+ self.xmlreport.closeblock()
+
+ self.xmlreport.openblock("clocksource")
+ self.xmlreport.taggedvalue('current', self.current_clocksource)
+ self.xmlreport.taggedvalue('available', self.available_clocksource)
+ self.xmlreport.closeblock()
+
+ self.xmlreport.openblock('hardware')
+ self.xmlreport.AppendXMLnodes(self.cputopology)
+ self.xmlreport.taggedvalue('numa_nodes', self.numanodes)
+ self.xmlreport.taggedvalue('memory_size', "%.3f" % self.memsize[0], {"unit": self.memsize[1]})
+ self.xmlreport.closeblock()
+
+ self.xmlreport.openblock('services', {'init': self.init})
+ for s in self.services:
+ self.xmlreport.taggedvalue("service", self.services[s], {"name": s})
+ self.xmlreport.closeblock()
+
+ keys = self.kthreads.keys()
+ if len(keys):
+ keys.sort()
+ self.xmlreport.openblock('kthreads')
+ for pid in keys:
+ self.xmlreport.taggedvalue('thread', self.kthreads[pid]['name'],
+ { 'policy' : self.kthreads[pid]['policy'],
+ 'priority' : self.kthreads[pid]['priority'],
+ })
+ self.xmlreport.closeblock()
+
+ modlist = util.get_modules()
+ if len(modlist):
+ self.xmlreport.openblock('kernelmodules')
+ for mod in modlist:
+ self.xmlreport.openblock('module')
+ self.xmlreport.taggedvalue('info', mod['modname'],
+ {'size': mod['modsize'],
+ 'state': mod['modstate'],
+ 'numusers': mod['numusers']})
+ if mod['usedby'] != '-':
+ self.xmlreport.openblock('usedby')
+ for ub in mod['usedby'].split(','):
+ if len(ub):
+ self.xmlreport.taggedvalue('module', ub, None)
+ self.xmlreport.closeblock()
+ self.xmlreport.closeblock()
+ self.xmlreport.closeblock()
+
+ #
+ # Retrieve configured IP addresses
+ #
+ self.xmlreport.openblock('network_config')
+
+ # Get the interface name for the IPv4 default gw
+ route = open('/proc/net/route')
+ defgw4 = None
+ if route:
+ rl = route.readline()
+ while rl != '' :
+ rl = route.readline()
+ splt = rl.split("\t")
+ # Only catch default route
+ if len(splt) > 2 and splt[2] != '00000000' and splt[1] == '00000000':
+ defgw4 = splt[0]
+ break
+ route.close()
+
+ # Make an interface tag for each device found
+ if hasattr(ethtool, 'get_interfaces_info'):
+ # Using the newer python-ethtool API (version >= 0.4)
+ for dev in ethtool.get_interfaces_info(ethtool.get_devices()):
+ if cmp(dev.device,'lo') == 0:
+ continue
+
+ self.xmlreport.openblock('interface',
+ {'device': dev.device,
+ 'hwaddr': dev.mac_address}
+ )
+
+ # Protcol configurations
+ if dev.ipv4_address:
+ self.xmlreport.openblock('IPv4',
+ {'ipaddr': dev.ipv4_address,
+ 'netmask': dev.ipv4_netmask,
+ 'broadcast': dev.ipv4_broadcast,
+ 'defaultgw': (defgw4 == dev.device) and '1' or '0'}
+ )
+ self.xmlreport.closeblock()
+
+ for ip6 in dev.get_ipv6_addresses():
+ self.xmlreport.openblock('IPv6',
+ {'ipaddr': ip6.address,
+ 'netmask': ip6.netmask,
+ 'scope': ip6.scope}
+ )
+ self.xmlreport.closeblock()
+ self.xmlreport.closeblock()
+ else: # Fall back to older python-ethtool API (version < 0.4)
+ ifdevs = ethtool.get_active_devices()
+ ifdevs.remove('lo')
+ ifdevs.sort()
+
+ for dev in ifdevs:
+ self.xmlreport.openblock('interface',
+ {'device': dev,
+ 'hwaddr': ethtool.get_hwaddr(dev)}
+ )
+ self.xmlreport.openblock('IPv4',
+ {'ipaddr': ethtool.get_ipaddr(dev),
+ 'netmask': ethtool.get_netmask(dev),
+ 'defaultgw': (defgw4 == dev) and '1' or '0'}
+ )
+ self.xmlreport.closeblock()
+ self.xmlreport.closeblock()
+ self.xmlreport.closeblock()
+
+ self.xmlreport.openblock('loads', {'load_average':str(accum / samples)})
+ for load in self.loads:
+ load.genxml(self.xmlreport)
+ self.xmlreport.closeblock()
+ self.cyclictest.genxml(self.xmlreport)
+
+ # now generate the dmidecode data for this host
+ d = dmi.DMIinfo(self.config.GetSection('rteval'))
+ d.genxml(self.xmlreport)
+
+ # Close the report - prepare for return the result
+ self.xmlreport.close()
+
+ # Write XML (or write XSLT parsed XML if xslt != None)
+ if self.xml != None:
+ self.xmlreport.Write(self.xml, xslt)
+ else:
+ # If no file is set, use stdout
+ self.xmlreport.Write("-", xslt) # libxml2 defines a filename as "-" to be stdout
+
+
+ def report(self):
+ "Create a screen report, based on a predefined XSLT template"
+ self.xmlreport.Write("-", self.config.xslt_report)
+
+ def XMLreport(self):
+ "Retrieves the complete rteval XML report as a libxml2.xmlDoc object"
+ return self.xmlreport.GetXMLdocument()
+
+ def show_report(self, xmlfile, xsltfile):
+ '''summarize a previously generated xml file'''
+ print "Loading %s for summarizing" % xmlfile
+
+ xsltfullpath = os.path.join(self.config.installdir, xsltfile)
+ if not os.path.exists(xsltfullpath):
+ raise RuntimeError, "can't find XSL template (%s)!" % xsltfullpath
+
+ xmlreport = xmlout.XMLOut('rteval', self.version)
+ xmlreport.LoadReport(xmlfile)
+ xmlreport.Write('-', xsltfullpath)
+ del xmlreport
+
+ def start_loads(self):
+ if len(self.loads) == 0:
+ raise RuntimeError, "start_loads: No loads defined!"
+ self.info ("starting loads:")
+ for l in self.loads:
+ l.start()
+ # now wait until they're all ready
+ self.info("waiting for ready from all loads")
+ ready=False
+ while not ready:
+ busy = 0
+ for l in self.loads:
+ if not l.isAlive():
+ raise RuntimeError, "%s died" % l.name
+ if not l.isReady():
+ busy += 1
+ self.debug("waiting for %s" % l.name)
+ if busy:
+ time.sleep(1.0)
+ else:
+ ready = True
+
+ def stop_loads(self):
+ if len(self.loads) == 0:
+ raise RuntimeError, "stop_loads: No loads defined!"
+ self.info("stopping loads: ")
+ for l in self.loads:
+ self.info("\t%s" % l.name)
+ l.stopevent.set()
+ l.join(2.0)
+
+ def make_report_dir(self):
+ t = self.start
+ i = 1
+ self.reportdir = os.path.join(self.workdir,
+ t.strftime("rteval-%Y%m%d-"+str(i)))
+ while os.path.exists(self.reportdir):
+ i += 1
+ self.reportdir = os.path.join(self.workdir,
+ t.strftime('rteval-%Y%m%d-'+str(i)))
+ if not os.path.isdir(self.reportdir):
+ os.mkdir(self.reportdir)
+ os.mkdir(os.path.join(self.reportdir, "logs"))
+ return self.reportdir
+
+ def get_dmesg(self):
+ dpath = "/var/log/dmesg"
+ if not os.path.exists(dpath):
+ print "dmesg file not found at %s" % dpath
+ return
+ shutil.copyfile(dpath, os.path.join(self.reportdir, "dmesg"))
+
+
+ def show_remaining_time(self, remaining):
+ r = int(remaining)
+ days = r / 86400
+ if days: r = r - (days * 86400)
+ hours = r / 3600
+ if hours: r = r - (hours * 3600)
+ minutes = r / 60
+ if minutes: r = r - (minutes * 60)
+ print "rteval time remaining: %d days, %d hours, %d minutes, %d seconds" % (days, hours, minutes, r)
+
+
+ def measure(self):
+ # Collect misc system info
+ self.baseos = util.get_base_os()
+ self.cputopology = self.get_cpu_topology()
+ self.numanodes = util.get_num_nodes()
+ self.memsize = util.get_memory_size()
+ (self.current_clocksource, self.available_clocksource) = util.get_clocksources()
+ self.services = self.get_services()
+ self.kthreads = self.get_kthreads()
+
+ onlyload = self.cmd_options.onlyload
+
+ builddir = os.path.join(self.workdir, 'rteval-build')
+ if not os.path.isdir(builddir): os.mkdir(builddir)
+ self.reportfile = os.path.join(self.reportdir, "summary.rpt")
+ self.xml = os.path.join(self.reportdir, "summary.xml")
+
+ # read in loads from the ini file
+ self.load_modules = []
+ loads = self.config.GetSection("loads")
+ for l in loads:
+ # hope to eventually have different kinds but module is only on
+ # for now (jcw)
+ if l[1].lower() == 'module':
+ self.info("importing load module %s" % l[0])
+ self.load_modules.append(__import__(l[0]))
+
+ self.info("setting up loads")
+ self.loads = []
+ params = {'workdir':self.workdir,
+ 'reportdir':self.reportdir,
+ 'builddir':builddir,
+ 'srcdir':self.config.srcdir,
+ 'verbose': self.config.verbose,
+ 'debugging': self.config.debugging,
+ 'numcores':self.numcores,
+ 'logging':self.config.logging,
+ 'memsize':self.memsize,
+ 'numanodes':self.numanodes,
+ 'duration':self.config.duration,
+ }
+
+ for m in self.load_modules:
+ self.config.AppendConfig(m.__name__, params)
+ self.info("creating load instance for %s" % m.__name__)
+ self.loads.append(m.create(self.config.GetSection(m.__name__)))
+
+ if not onlyload:
+ self.config.AppendConfig('cyclictest', params)
+ self.info("setting up cyclictest")
+ self.cyclictest = cyclictest.Cyclictest(params=self.config.GetSection('cyclictest'))
+
+ nthreads = 0
+ try:
+ # start the loads
+ self.start_loads()
+
+ print "rteval run on %s started at %s" % (os.uname()[2], time.asctime())
+ print "started %d loads on %d cores" % (len(self.loads), self.numcores),
+ if self.numanodes > 1:
+ print " with %d numa nodes" % self.numanodes
+ else:
+ print ""
+ print "Run duration: %d seconds" % self.config.duration
+
+ start = datetime.now()
+
+ if not onlyload:
+ # start the cyclictest thread
+ self.info("starting cyclictest")
+ self.cyclictest.start()
+
+ # turn loose the loads
+ self.info("sending start event to all loads")
+ for l in self.loads:
+ l.startevent.set()
+ nthreads += 1
+
+ accum = 0.0
+ samples = 0
+
+ report_interval = int(self.config.GetSection('rteval').report_interval)
+
+ # wait for time to expire or thread to die
+ signal.signal(signal.SIGINT, sigint_handler)
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ self.info("waiting for duration (%f)" % self.config.duration)
+ stoptime = (time.time() + self.config.duration)
+ currtime = time.time()
+ rpttime = currtime + report_interval
+ loadcount = 5
+ while (currtime <= stoptime) and not sigint_received:
+ time.sleep(1.0)
+ if not onlyload and not self.cyclictest.isAlive():
+ raise RuntimeError, "cyclictest thread died!"
+ if len(threading.enumerate()) < nthreads:
+ raise RuntimeError, "load thread died!"
+ if not loadcount:
+ # open the loadavg /proc entry
+ p = open("/proc/loadavg")
+ load = float(p.readline().split()[0])
+ p.close()
+ accum += load
+ samples += 1
+ loadcount = 5
+ #self.debug("current loadavg: %f, running avg: %f (load: %f, samples: %d)" % \
+ # (load, accum/samples, load, samples))
+ else:
+ loadcount -= 1
+ if currtime >= rpttime:
+ left_to_run = stoptime - currtime
+ self.show_remaining_time(left_to_run)
+ rpttime = currtime + report_interval
+ print "load average: %.2f" % (accum / samples)
+ currtime = time.time()
+ self.debug("out of measurement loop")
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ signal.signal(signal.SIGTERM, signal.SIG_DFL)
+
+ except RuntimeError, e:
+ print "Runtime error during measurement: %s", e
+ raise
+
+ finally:
+ if not onlyload:
+ # stop cyclictest
+ self.cyclictest.stopevent.set()
+
+ # stop the loads
+ self.stop_loads()
+
+ print "stopping run at %s" % time.asctime()
+ if not onlyload:
+ # wait for cyclictest to finish calculating stats
+ self.cyclictest.finished.wait()
+ self.genxml(datetime.now() - start, accum, samples)
+ self.report()
+ if self.config.sysreport:
+ self.run_sysreport()
+
+
+ def XMLRPC_Send(self):
+ "Sends the report to a given XML-RPC host. Returns 0 on success or 2 on submission failure."
+
+ if not self.config.xmlrpc:
+ return 2
+
+ url = "http://%s/rteval/API1/" % self.config.xmlrpc
+ attempt = 0
+ exitcode = 2 # Presume failure
+ warning_sent = False
+ while attempt < 6:
+ try:
+ client = rtevalclient.rtevalclient(url)
+ print "Submitting report to %s" % url
+ rterid = client.SendReport(self.xmlreport.GetXMLdocument())
+ print "Report registered with submission id %i" % rterid
+ attempt = 10
+ exitcode = 0 # Success
+ except socket.error:
+ if (self.mailer is not None) and (not warning_sent):
+ self.mailer.SendMessage("[RTEVAL:WARNING] Failed to submit report to XML-RPC server",
+ "Server %s did not respond. Not giving up yet."
+ % self.config.xmlrpc)
+ warning_sent = True
+
+ attempt += 1
+ if attempt > 5:
+ break # To avoid sleeping before we abort
+
+ print "Failed sending report. Doing another attempt(%i) " % attempt
+ time.sleep(attempt*5*60) # Incremental sleep - sleep attempts*5 minutes
+
+ except Exception, err:
+ raise err
+
+ if (self.mailer is not None):
+ # Send final result messages
+ if exitcode == 2:
+ self.mailer.SendMessage("[RTEVAL:FAILURE] Failed to submit report to XML-RPC server",
+ "Server %s did not respond at all after %i attempts."
+ % (self.config.xmlrpc, attempt - 1))
+ elif (exitcode == 0) and warning_sent:
+ self.mailer.SendMessage("[RTEVAL:SUCCESS] XML-RPC server available again",
+ "Succeeded to submit the report to %s in the end."
+ % (self.config.xmlrpc))
+ return exitcode
+
+
+ def tar_results(self):
+ if not os.path.isdir(self.reportdir):
+ raise RuntimeError, "no such directory: %s" % self.reportdir
+ import tarfile
+ dirname = os.path.dirname(self.reportdir)
+ rptdir = os.path.basename(self.reportdir)
+ cwd = os.getcwd()
+ os.chdir(dirname)
+ try:
+ t = tarfile.open(rptdir + ".tar.bz2", "w:bz2")
+ t.add(rptdir)
+ t.close()
+ except:
+ os.chdir(cwd)
+
+ def summarize(self, file):
+ isarchive = False
+ summary = file
+ if file.endswith(".tar.bz2"):
+ import tarfile
+ try:
+ t = tarfile.open(file)
+ except:
+ print "Don't know how to summarize %s (tarfile open failed)" % file
+ return
+ element = None
+ for f in t.getnames():
+ if f.find('summary.xml') != -1:
+ element = f
+ break
+ if element == None:
+ print "No summary.xml found in tar archive %s" % file
+ return
+ tmp = tempfile.gettempdir()
+ self.debug("extracting %s from %s for summarizing" % (element, file))
+ t.extract(element, path=tmp)
+ summary = os.path.join(tmp, element)
+ isarchive = True
+ self.show_report(summary, 'rteval_text.xsl')
+ if isarchive:
+ os.unlink(summary)
+
+ def rteval(self):
+ ''' main function for rteval'''
+ retval = 0;
+
+ # Parse initial DMI decoding errors
+ dmi.ProcessWarnings()
+
+ # if --summarize was specified then just parse the XML, print it and exit
+ if self.cmd_options.summarize or self.cmd_options.rawhistogram:
+ if len(self.cmd_arguments) < 1:
+ raise RuntimeError, "Must specify at least one XML file with --summarize!"
+
+ for x in self.cmd_arguments:
+ if self.cmd_options.summarize:
+ self.summarize(x)
+ elif self.cmd_options.rawhistogram:
+ self.show_report(x, 'rteval_histogram_raw.xsl')
+
+ sys.exit(0)
+
+ if os.getuid() != 0:
+ print "Must be root to run rteval!"
+ sys.exit(-1)
+
+ self.debug('''rteval options:
+ workdir: %s
+ loaddir: %s
+ reportdir: %s
+ verbose: %s
+ debugging: %s
+ logging: %s
+ duration: %f
+ sysreport: %s
+ inifile: %s''' % (self.workdir, self.config.srcdir, self.reportdir, self.config.verbose,
+ self.config.debugging, self.config.logging, self.config.duration,
+ self.config.sysreport, self.inifile))
+
+ if not os.path.isdir(self.workdir):
+ raise RuntimeError, "work directory %d does not exist" % self.workdir
+
+ # create our report directory
+ try:
+ self.make_report_dir()
+ except:
+ print "Cannot create the report dir!"
+ print "(is this an NFS filesystem with rootsquash turned on?)"
+ sys.exit(-1)
+
+ self.measure()
+
+ # if --xmlrpc-submit | -X was given, send our report to this host
+ if self.config.xmlrpc:
+ retval = self.XMLRPC_Send()
+
+ self.get_dmesg()
+ self.tar_results()
+
+ return retval
+
+if __name__ == '__main__':
+ import pwd, grp
+
+ try:
+ # Parse initial DMI decoding errors
+ dmi.ProcessWarnings()
+
+ rteval = RtEval(sys.argv[1:])
+ ec = rteval.rteval()
+ rteval.debug("exiting with exit code: %d" % ec)
+ sys.exit(ec)
+ except KeyboardInterrupt:
+ sys.exit(0)
diff --git a/setup.py b/setup.py
index cd8d08f..f5dc146 100644
--- a/setup.py
+++ b/setup.py
@@ -44,15 +44,15 @@
license = "GPLv2",
long_description =
"""\
-The rteval script is used to judge the behavior of a hardware
+The rteval script is used to judge the behavior of a hardware
platform while running a Realtime Linux kernel under a moderate
-to heavy load.
+to heavy load.
-Provides control logic for starting a system load and then running a
+Provides control logic for starting a system load and then running a
response time measurement utility (cyclictest) for a specified amount
of time. When the run is finished, the sample data from cyclictest is
analyzed for standard statistical measurements (i.e mode, median, range,
-mean, variance and standard deviation) and a report is generated.
+mean, variance and standard deviation) and a report is generated.
""",
packages = ["rteval",
"rteval.modules",