modprobe.py:
authorMarc Fiuczynski <mef@cs.princeton.edu>
Thu, 13 Nov 2008 22:49:47 +0000 (22:49 +0000)
committerMarc Fiuczynski <mef@cs.princeton.edu>
Thu, 13 Nov 2008 22:49:47 +0000 (22:49 +0000)
This new python module implements a class to parse /etc/modprobe.conf
and change/update/add aliases and options.  This is generally needed
to support the NodeNetworkSetting for "DRIVER".  For now I am
leveraging this to add NIC bonding.

net.py:
Added InitInterfaces() which was originally based on the code found in
the BootManager/source/steps/WriteNetworkConfig.py.  My original
motivation was to support "ALIAS" setup dynamically so that one could
assign a slice a separate IP address without needing to reboot the
node.

Added support to more generally handle "CFGOPTIONS", which are the
name=value pairs that are supposed to be dumped into a
/etc/sysconfig/network-scripts/ifcfg-$dev file.

Added support for "DRIVER".  This code calls the modprobe
functionality mentioned above.

Added support to let aliases be associated by "IFNAME" rather than
just by "HWADDR".  E.g., it is possible to associate an alias with
eth0 rather than having to figure out its explicitl macaddr.

Added suport to add, change, remove
/etc/sysconfig/network-scripts/ifcfg-$dev files and then appropriately
call ifdown and ifup.

modprobe.py [new file with mode: 0644]
net.py

diff --git a/modprobe.py b/modprobe.py
new file mode 100644 (file)
index 0000000..d7fa2e3
--- /dev/null
@@ -0,0 +1,130 @@
+#
+# $Id$
+#
+
+"""Modprobe is a utility to read/modify/write /etc/modprobe.conf"""
+
+import os
+
+class Modprobe():
+    def __init__(self,filename="/etc/modprobe.conf"):
+        self.conffile = {}
+        self.origconffile = {}
+        for keyword in ("alias","options","install","remove","blacklist","MODULES"):
+            self.conffile[keyword]={}
+        self.filename = filename
+
+    def input(self,filename=None):
+        if filename==None: filename=self.filename
+        fb = file(filename,"r")
+        for line in fb.readlines():
+            parts = line.split()
+            command = parts[0].lower()
+
+            table = self.conffile.get(command,None)
+            if table == None:
+                print "WARNING: command %s not recognize. Ignoring!" % command
+                continue
+
+            if command == "alias":
+                wildcard=parts[1]
+                modulename=parts[2]
+                self.aliasset(wildcard,modulename)
+                options=''
+                if len(parts)>3:
+                    options=" ".join(parts[3:])
+                    self.optionsset(modulename,options)
+                    self.conffile['MODULES']={}
+                self.conffile['MODULES'][modulename]=options
+            else:
+                modulename=parts[1]
+                rest=" ".join(parts[2:])
+                self._set(command,modulename,rest)
+                if command == "options":
+                    self.conffile['MODULES'][modulename]=rest
+
+        self.origconffile = self.conffile.copy()
+                
+    def _get(self,command,key):
+        return self.conffile[command].get(key,None)
+
+    def _set(self,command,key,value):
+        self.conffile[command][key]=value
+
+    def aliasget(self,key):
+        return self._get('alias',key)
+
+    def optionsget(self,key):
+        return self._get('options',key)
+
+    def aliasset(self,key,value):
+        self._set("alias",key,value)
+
+    def optionsset(self,key,value):
+        self._set("options",key,value)
+        
+    def _comparefiles(self,a,b):
+        try:
+            if not os.path.exists(a): return False
+            fb = open(a)
+            buf_a = fb.read()
+            fb.close()
+
+            if not os.path.exists(b): return False
+            fb = open(b)
+            buf_b = fb.read()
+            fb.close()
+
+            return buf_a == buf_b
+        except IOError, e:
+            return False
+
+    def output(self,filename="/etc/modprobe.conf",program="NodeManager"):
+        tmpnam = os.tmpnam()
+        fb = file(tmpnam,"w")
+        fb.write("# Written out by %s\n" % program)
+
+        for command in ("alias","options","install","remove","blacklist"):
+            table = self.conffile[command]
+            keys = table.keys()
+            keys.sort()
+            for k in keys:
+                v = table[k]
+                fb.write("%s %s %s\n" % (command,k,v))
+
+        fb.close()
+        if not self._comparefiles(tmpnam,filename):
+            os.rename(tmpnam,filename)
+            os.chmod(filename,0644)
+            return True
+        else:
+            return False
+
+    def probe(self,name):
+        o = os.popen("/sbin/modprobe %s" % name)
+        o.close()
+
+    def checkmodules(self):
+        syspath="/sys/module"
+        modules = os.listdir(syspath)
+        for module in modules:
+            path="%/%s/parameters"%(syspath,module)
+            if os.path.exists(path):
+                ps=os.listdir(path)
+                parameters={}
+                for p in ps:
+                    fb = file("%s/%s"%(path,p),"r")
+                    parameters[p]=fb.readline()
+                    fb.close()
+         
+if __name__ == '__main__':
+    import sys
+    if len(sys.argv)>1:
+        m = Modprobe(sys.argv[1])
+    else:
+        m = Modprobe()
+
+    m.input()
+    m.aliasset("bond0","bonding")
+    m.optionsset("bond0","miimon=100")
+    m.output("/tmp/x")
diff --git a/net.py b/net.py
index e0f9c12..394c9d7 100644 (file)
--- a/net.py
+++ b/net.py
@@ -4,14 +4,14 @@
 
 """network configuration"""
 
-import sioc
-import bwlimit
-import logger
-import string
-import iptables
-import os
+# system provided modules
+import os, string, time, socket, modprobe
+
+# local modules
+import sioc, bwlimit, logger, iptables
 
 def GetSlivers(plc, data):
+    InitInterfaces(plc, data)
     InitNodeLimit(data)
     InitI2(plc, data)
     InitNAT(plc, data)
@@ -29,9 +29,11 @@ def InitNodeLimit(data):
     for network in data['networks']:
         # Get interface name preferably from MAC address, falling
         # back on IP address.
-        if macs.has_key(network['mac']):
-            dev = macs[network['mac'].lower()]
-        elif ips.has_key(network['ip']):
+        hwaddr=network['mac']
+        if hwaddr <> None: hwaddr=hwaddr.lower()
+        if hwaddr in macs:
+            dev = macs[network['mac']]
+        elif network['ip'] in ips:
             dev = ips[network['ip']]
         else:
             logger.log('%s: no such interface with address %s/%s' % (network['hostname'], network['ip'], network['mac']))
@@ -92,9 +94,11 @@ def InitNAT(plc, data):
     for network in data['networks']:
         # Get interface name preferably from MAC address, falling
         # back on IP address.
-        if macs.has_key(network['mac']):
-            dev = macs[network['mac'].lower()]
-        elif ips.has_key(network['ip']):
+        hwaddr=network['mac']
+        if hwaddr <> None: hwaddr=hwaddr.lower()
+        if hwaddr in macs:
+            dev = macs[network['mac']]
+        elif network['ip'] in ips:
             dev = ips[network['ip']]
         else:
             logger.log('%s: no such interface with address %s/%s' % (network['hostname'], network['ip'], network['mac']))
@@ -104,7 +108,7 @@ def InitNAT(plc, data):
             settings = plc.GetNodeNetworkSettings({'nodenetwork_setting_id': network['nodenetwork_setting_ids']})
         except:
             continue
-        # XXX arbitrary names
+
         for setting in settings:
             if setting['category'].upper() != 'FIREWALL':
                 continue
@@ -126,5 +130,279 @@ def InitNAT(plc, data):
                     ipt.add_pf(fields)
     ipt.commit()
 
+def InitInterfaces(plc, data):
+    sysconfig = "/etc/sysconfig/network-scripts"
+
+    # query running network interfaces
+    devs = sioc.gifconf()
+    ips = dict(zip(devs.values(), devs.keys()))
+    macs = {}
+    for dev in devs:
+        macs[sioc.gifhwaddr(dev).lower()] = dev
+
+    # assume data['networks'] contains this node's NodeNetworks
+    interfaces = {}
+    interface = 1
+    hostname = data.get('hostname',socket.gethostname())
+    networks = data.get('networks',())
+    failedToGetSettings = False
+    for network in networks:
+       logger.log('interface %d: %s'%(interface,network))
+       logger.log('macs = %s' % macs)
+        logger.log('ips = %s' % ips)
+        # Get interface name preferably from MAC address, falling back
+        # on IP address.
+        hwaddr=network['mac']
+        if hwaddr <> None: hwaddr=hwaddr.lower()
+        if hwaddr in macs:
+            orig_ifname = macs[hwaddr]
+        elif network['ip'] in ips:
+            orig_ifname = ips[network['ip']]
+        else:
+            orig_ifname = None
+
+       if orig_ifname:
+                       logger.log('orig_ifname = %s' % orig_ifname)
+       
+        inter = {}
+        inter['ONBOOT']='yes'
+        inter['USERCTL']='no'
+        if network['mac']:
+            inter['HWADDR'] = network['mac']
+
+        if network['method'] == "static":
+            inter['BOOTPROTO'] = "static"
+            inter['IPADDR'] = network['ip']
+            inter['NETMASK'] = network['netmask']
+
+        elif network['method'] == "dhcp":
+            inter['BOOTPROTO'] = "dhcp"
+            if network['hostname']:
+                inter['DHCP_HOSTNAME'] = network['hostname']
+            else:
+                inter['DHCP_HOSTNAME'] = hostname 
+            if not network['is_primary']:
+                inter['DHCLIENTARGS'] = "-R subnet-mask"
+
+        if len(network['nodenetwork_setting_ids']) > 0:
+            try:
+                settings = plc.GetNodeNetworkSettings({'nodenetwork_setting_id':
+                                                       network['nodenetwork_setting_ids']})
+            except:
+                logger.log("FATAL: failed call GetNodeNetworkSettings({'nodenetwork_setting_id':{%s})"% \
+                           network['nodenetwork_setting_ids'])
+                failedToGetSettings = True
+                continue # on to the next network
+
+            for setting in settings:
+                # to explicitly set interface name
+                settingname = setting['name'].upper()
+                if settingname in ('IFNAME','ALIAS','CFGOPTIONS','DRIVER'):
+                    inter[settingname]=setting['value']
+                else:
+                    logger.log("WARNING: ignored setting named %s"%setting['name'])
+
+        # support aliases to interfaces either by name or HWADDR
+        if 'ALIAS' in inter:
+            if 'HWADDR' in inter:
+                hwaddr = inter['HWADDR'].lower()
+                del inter['HWADDR']
+                if hwaddr in macs:
+                    hwifname = macs[hwaddr]
+                    if ('IFNAME' in inter) and inter['IFNAME'] <> hwifname:
+                        logger.log("WARNING: alias ifname (%s) and hwaddr ifname (%s) do not match"%\
+                                       (inter['IFNAME'],hwifname))
+                        inter['IFNAME'] = hwifname
+                else:
+                    logger.log('WARNING: mac addr %s for alias not found' %(hwaddr,alias))
+
+            if 'IFNAME' in inter:
+                # stupid RH /etc/sysconfig/network-scripts/ifup-aliases:new_interface()
+                # checks if the "$DEVNUM" only consists of '^[0-9A-Za-z_]*$'. Need to make
+                # our aliases compliant.
+                parts = inter['ALIAS'].split('_')
+                isValid=True
+                for part in parts:
+                    isValid=isValid and part.isalnum()
+
+                if isValid:
+                    interfaces["%s:%s" % (inter['IFNAME'],inter['ALIAS'])] = inter 
+                else:
+                    logger.log("WARNING: interface alias (%s) not a valid string for RH ifup-aliases"% inter['ALIAS'])
+            else:
+                logger.log("WARNING: interface alias (%s) not matched to an interface"% inter['ALIAS'])
+            interface -= 1
+        else:
+            if ('IFNAME' not in inter) and not orig_ifname:
+                ifname="eth%d" % (interface-1)
+                # should check if $ifname is an eth already defines
+                if os.path.exists("%s/ifcfg-%s"%(sysconfig,ifname)):
+                    logger.log("WARNING: possibly blowing away %s configuration"%ifname)
+            else:
+               if ('IFNAME' not in inter) and orig_ifname:
+                    ifname = orig_ifname
+                else:
+                    ifname = inter['IFNAME']
+                interface -= 1
+            interfaces[ifname] = inter
+                
+    m = modprobe.Modprobe()
+    m.input("/etc/modprobe.conf")
+    for (dev, inter) in interfaces.iteritems():
+        # get the driver string "moduleName option1=a option2=b"
+        driver=inter.get('DRIVER','')
+        if driver <> '':
+            driver=driver.split()
+            kernelmodule=driver[0]
+            m.aliasset(dev,kernelmodule)
+            options=" ".join(driver[1:])
+            if options <> '':
+                m.optionsset(dev,options)
+    m.output("/etc/modprobe.conf")
+
+    # clean up after any ifcfg-$dev script that's no longer listed as
+    # part of the NodeNetworks associated with this node
+
+    # list all network-scripts
+    files = os.listdir(sysconfig)
+
+    # filter out the ifcfg-* files
+    ifcfgs=[]
+    for f in files:
+        if f.find("ifcfg-") == 0:
+            ifcfgs.append(f)
+
+    # remove loopback (lo) from ifcfgs list
+    lo = "ifcfg-lo"
+    if lo in ifcfgs: ifcfgs.remove(lo)
+
+    # remove known devices from icfgs list
+    for (dev, inter) in interfaces.iteritems():
+        ifcfg = 'ifcfg-'+dev
+        if ifcfg in ifcfgs: ifcfgs.remove(ifcfg)
+
+    # delete the remaining ifcfgs from 
+    deletedSomething = False
+
+    if not failedToGetSettings:
+        for ifcfg in ifcfgs:
+            dev = ifcfg[len('ifcfg-'):]
+            path = "%s/ifcfg-%s" % (sysconfig,dev)
+            logger.log("removing %s %s"%(dev,path))
+            ifdown = os.popen("/sbin/ifdown %s" % dev)
+            ifdown.close()
+            deletedSomething=True
+            os.unlink(path)
+
+    # wait a bit for the one or more ifdowns to have taken effect
+    if deletedSomething:
+        time.sleep(2)
+
+    # Process ifcg-$dev changes / additions
+    newdevs = []
+    for (dev, inter) in interfaces.iteritems():
+        tmpnam = os.tmpnam()
+        f = file(tmpnam, "w")
+        f.write("# Autogenerated by NodeManager/net.py.... do not edit!\n")
+        if 'DRIVER' in inter:
+            f.write("# using %s driver for device %s\n" % (inter['DRIVER'],dev))
+        f.write('DEVICE="%s"\n' % dev)
+        
+        # print the configuration values
+        for (key, val) in inter.iteritems():
+            if key not in ('IFNAME','ALIAS','CFGOPTIONS','DRIVER'):
+                f.write('%s="%s"\n' % (key, val))
+
+        # print the configuration specific option values (if any)
+        if 'CFGOPTIONS' in inter:
+            cfgoptions = inter['CFGOPTIONS']
+            f.write('#CFGOPTIONS are %s\n' % cfgoptions)
+            for cfgoption in cfgoptions.split():
+                key,val = cfgoption.split('=')
+                key=key.strip()
+                key=key.upper()
+                val=val.strip()
+                f.write('%s="%s"\n' % (key,val))
+        f.close()
+
+        # compare whether two files are the same
+        def comparefiles(a,b):
+            try:
+               logger.log("comparing %s with %s" % (a,b))
+                if not os.path.exists(a): return False
+                fb = open(a)
+                buf_a = fb.read()
+                fb.close()
+
+                if not os.path.exists(b): return False
+                fb = open(b)
+                buf_b = fb.read()
+                fb.close()
+
+                return buf_a == buf_b
+            except IOError, e:
+                return False
+
+        path = "%s/ifcfg-%s" % (sysconfig,dev)
+        if not os.path.exists(path):
+            logger.log('adding configuration for %s' % dev)
+            # add ifcfg-$dev configuration file
+            os.rename(tmpnam,path)
+            os.chmod(path,0644)
+            newdevs.append(dev)
+            
+        elif not comparefiles(tmpnam,path):
+            logger.log('Configuration change for %s' % dev)
+            logger.log('ifdown %s' % dev)
+            # invoke ifdown for the old configuration
+            p = os.popen("/sbin/ifdown %s" % dev)
+            p.close()
+            # wait a few secs for ifdown to complete
+            time.sleep(2)
+
+            logger.log('replacing configuration for %s' % dev)
+            # replace ifcfg-$dev configuration file
+            os.rename(tmpnam,path)
+            os.chmod(path,0644)
+            newdevs.append(dev)
+        else:
+            # tmpnam & path are identical
+            os.unlink(tmpnam)
+
+    for dev in newdevs:
+        cfgvariables = {}
+        fb = file("%s/ifcfg-%s"%(sysconfig,dev),"r")
+        for line in fb.readlines():
+            parts = line.split()
+            if parts[0][0]=="#":continue
+            if parts[0].find('='):
+                name,value = parts[0].split('=')
+                # clean up name & value
+                name = name.strip()
+                value = value.strip()
+                value = value.strip("'")
+                value = value.strip('"')
+                cfgvariables[name]=value
+        fb.close()
+
+        def getvar(name):
+            if name in cfgvariables:
+                value=cfgvariables[name]
+                value = value.lower()
+                return value
+            return ''
+
+        # skip over device configs with ONBOOT=no
+        if getvar("ONBOOT") == 'no': continue
+
+        # don't bring up slave devices, the network scripts will
+        # handle those correctly
+        if getvar("SLAVE") == 'yes': continue
+
+        logger.log('bringing up %s' % dev)
+        p = os.popen("/sbin/ifup %s" % dev)
+        # check for failure?
+        p.close()
+
 def start(options, config):
     pass