add systopology.py

This file adds classes for CpuList, NumaNodes and SysTopology. The SysTopology class
is intended to be instantiated early in the rteval run and provide information to
modules that will allow them to intelligently place loads and measurement programs
based on the system resources.

Signed-off-by: Clark Williams <williams@redhat.com>
diff --git a/rteval/rtevalConfig.py b/rteval/rtevalConfig.py
index 5a4aa5f..47488a0 100644
--- a/rteval/rtevalConfig.py
+++ b/rteval/rtevalConfig.py
@@ -33,7 +33,7 @@
 import os, sys
 import ConfigParser
 from Log import Log
-
+from sysinfo.systopology import SysTopology
 
 def get_user_name():
     name = os.getenv('SUDO_USER')
@@ -188,6 +188,11 @@
         self.__config_files = []
         self.__logger = logger
 
+        # get our system topology info
+        self.__systopology = SysTopology()
+        # debug
+        print self.__systopology
+
         # Import the default config first
         for sect, vals in default_config.items():
             self.__update_section(sect, vals)
diff --git a/rteval/sysinfo/__init__.py b/rteval/sysinfo/__init__.py
index fe5fbd6..f1b6a03 100644
--- a/rteval/sysinfo/__init__.py
+++ b/rteval/sysinfo/__init__.py
@@ -32,9 +32,9 @@
 from memory import MemoryInfo
 from osinfo import OSInfo
 from network import NetworkInfo
+import systopology
 import dmi
 
-
 class SystemInfo(KernelInfo, SystemServices, dmi.DMIinfo, CPUtopology, MemoryInfo, OSInfo, NetworkInfo):
     def __init__(self, config, logger=None):
         self.__logger = logger
diff --git a/rteval/sysinfo/dmi.py b/rteval/sysinfo/dmi.py
index 84ca97f..f8e2500 100644
--- a/rteval/sysinfo/dmi.py
+++ b/rteval/sysinfo/dmi.py
@@ -27,8 +27,9 @@
 
 import sys, os
 import libxml2, lxml.etree
-from rteval import rtevalConfig, xmlout
 from rteval.Log import Log
+from rteval import xmlout
+from rteval import rtevalConfig
 
 try:
     import dmidecode
@@ -38,7 +39,7 @@
     pass
 
 def ProcessWarnings():
-    
+
     if not hasattr(dmidecode, 'get_warnings'):
         return
 
diff --git a/rteval/sysinfo/systopology.py b/rteval/sysinfo/systopology.py
new file mode 100644
index 0000000..0295276
--- /dev/null
+++ b/rteval/sysinfo/systopology.py
@@ -0,0 +1,243 @@
+# -*- coding: utf-8 -*-
+#
+#   Copyright 2016 - 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.,
+#   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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, sys
+import os.path
+import glob
+
+def _sysread(path, obj):
+    fp = open(os.path.join(path,obj), "r")
+    return fp.readline().strip()
+
+#
+# class to provide access to a list of cpus
+#
+
+class CpuList(object):
+    "Object that represents a group of system cpus"
+
+    cpupath = '/sys/devices/system/cpu'
+
+    def __init__(self, cpulist):
+        if type(cpulist) is list:
+            self.cpulist = cpulist
+        elif type(cpulist) is str:
+            self.cpulist = self.__expand_cpulist(cpulist)
+        self.cpulist.sort()
+
+    def __str__(self):
+        return self.__collapse_cpulist(self.cpulist)
+
+    def __contains__(self, cpu):
+        return cpu in self.cpulist
+
+    def __len__(self):
+        return len(self.cpulist)
+
+
+    # return the index of the last element of a sequence
+    # that steps by one
+    def __longest_sequence(self, cpulist):
+        lim = len(cpulist)
+        for idx,val in enumerate(cpulist):
+            if idx+1 == lim:
+                break
+            if int(cpulist[idx+1]) != (int(cpulist[idx])+1):
+                return idx
+        return lim - 1
+
+
+    #
+    # collapse a list of cpu numbers into a string range
+    # of cpus (e.g. 0-5, 7, 9)
+    #
+    def __collapse_cpulist(self, cpulist):
+        if len(cpulist) == 0:
+            return ""
+        idx = self.__longest_sequence(cpulist)
+        if idx == 0:
+            seq = str(cpulist[0])
+        else:
+            if idx == 1:
+                seq = "%d,%d" % (cpulist[0], cpulist[idx])
+            else:
+                seq = "%d-%d" % (cpulist[0], cpulist[idx])
+
+        rest = self.__collapse_cpulist(cpulist[idx+1:])
+        if rest == "":
+            return seq
+        return ",".join((seq, rest))
+
+    # expand a string range into a list
+    # don't error check against online cpus
+    def __expand_cpulist(self, cpulist):
+        '''expand a range string into an array of cpu numbers'''
+        result = []
+        for part in cpulist.split(','):
+            if '-' in part:
+                a, b = part.split('-')
+                a, b = int(a), int(b)
+                result.extend(range(a, b + 1))
+            else:
+                a = int(part)
+                result.append(a)
+        return [ int(i) for i in list(set(result)) ]
+
+    # returns the list of cpus tracked
+    def getcpulist(self):
+        return self.cpulist
+
+    # check whether cpu n is online
+    def isonline(self, n):
+        if n not in self.cpulist:
+            raise RuntimeError, "invalid cpu number %d" % n
+        if n == 0:
+            return True
+        path = os.path.join(CpuList.cpupath,'cpu%d' % n)
+        if os.path.exists(path):
+            return _sysread(path, "online") == 1
+        return False
+
+#
+# class to abstract access to NUMA nodes in /sys filesystem
+#
+
+class NumaNode(object):
+    "class representing a system NUMA node"
+
+    # constructor argument is the full path to the /sys node file
+    # e.g. /sys/devices/system/node/node0
+    def __init__(self, path):
+        self.path = path
+        self.nodeid = int(os.path.basename(path)[4:].strip())
+        self.cpus = CpuList(_sysread(self.path, "cpulist"))
+        self.getmeminfo()
+
+    # function for the 'in' operator
+    def __contains__(self, cpu):
+        return cpu in self.cpus
+
+    # allow the 'len' builtin
+    def __len__(self):
+        return len(self.cpus)
+
+    # string representation of the cpus for this node
+    def __str__(self):
+        return self.getcpustr()
+
+    # read info about memory attached to this node
+    def getmeminfo(self):
+        self.meminfo = {}
+        for l in open(os.path.join(self.path, "meminfo"), "r"):
+            elements = l.split()
+            key=elements[2][0:-1]
+            val=int(elements[3])
+            if len(elements) == 5 and elements[4] == "kB":
+                val *= 1024
+            self.meminfo[key] = val
+
+    # return list of cpus for this node as a string
+    def getcpustr(self):
+        return str(self.cpus)
+
+    # return list of cpus for this node
+    def getcpulist(self):
+        return self.cpus.getcpulist()
+
+#
+# Class to abstract the system topology of numa nodes and cpus
+#
+class SysTopology(object):
+    "Object that represents the system's NUMA-node/cpu topology"
+
+    cpupath = '/sys/devices/system/cpu'
+    nodepath = '/sys/devices/system/node'
+
+    def __init__(self):
+        self.nodes = {}
+        self.getinfo()
+
+    def __len__(self):
+        return len(self.nodes.keys())
+
+    def __str__(self):
+        s = "%d node system" % len(self.nodes.keys())
+        s += " (%d cores per node)" % (len(self.nodes[self.nodes.keys()[0]]))
+        return s
+
+    # inplement the 'in' function
+    def __contains__(self, node):
+        for n in self.nodes:
+            if self.nodes[n].nodeid == node:
+                return True
+        return False
+
+    # allow indexing for the nodes
+    def __getitem__(self, key):
+        return self.nodes[key]
+
+    # allow iteration over the cpus for the node
+    def __iter__(self):
+        self.current = 0
+        return self
+
+    # iterator function
+    def next(self):
+        if self.current >= len(self.nodes):
+            raise StopIteration
+        n = self.nodes[self.current]
+        self.current += 1
+        return n
+
+    def getinfo(self):
+        nodes = glob.glob(os.path.join(SysTopology.nodepath, 'node[0-9]*'))
+        if not nodes:
+            raise RuntimeError, "No valid nodes found in %s!" % SysTopology.nodepath
+        nodes.sort()
+        for n in nodes:
+            node = int(os.path.basename(n)[4:])
+            self.nodes[node] = NumaNode(n)
+
+    def getnodes(self):
+        return self.nodes.keys()
+
+    def getcpus(self, node):
+        return self.nodes[node]
+
+
+
+if __name__ == "__main__":
+
+    def unit_test():
+        s = SysTopology()
+        print s
+        print "number of nodes: %d" % len(s)
+        for n in s:
+            print "node[%d]: %s" % (n.nodeid, n)
+        print "system has numa node 0: %s" % (0 in s)
+        print "system has numa node 2: %s" % (2 in s)
+        print "system has numa node 24: %s" % (24 in s)
+
+    unit_test()