[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[minion-cvs] Test, document, and debug configuration code.



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.seul.org:/tmp/cvs-serv32470/minion/lib/mixminion

Modified Files:
	Common.py Config.py Crypto.py test.py 
Log Message:
Test, document, and debug configuration code.

Make entropy source configurable.

Remove misconceived _parseDir functionality.


Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.9
retrieving revision 1.10
diff -u -d -r1.9 -r1.10
--- Common.py	25 Jul 2002 15:52:57 -0000	1.9
+++ Common.py	26 Jul 2002 15:47:20 -0000	1.10
@@ -174,17 +174,20 @@
         import mixminion.Config as Config
         config = Config.getConfig()
         self.handlers = []
-        if config == None or not config.has_section('Server'):
+        if config == None or not config.has_section("Server"):
             self.setMinSeverity("WARN")
             self.addHandler(ConsoleLogTarget(sys.stderr))
         else:
-            self.setMinSeverity(config['Server']['LogLevel'])
-            if config['Server']['EchoMessages']:
+            self.setMinSeverity(config['Server'].get('LogLevel', "WARN"))
+            logfile = config['Server'].get('LogFile',None)
+            if logfile is None:
+                homedir = config['Server']['Homedir']
+                if homedir:
+                    logfile = os.path.join(homedir, "log")
+            if not logfile or config['Server'].get('EchoMessages',0):
                 self.addHandler(ConsoleLogTarget(sys.stderr))
-            logfile = config['Server']['LogFile']
-            if  logfile is not None:   
-                logfile = os.path.join(config['Server']['Homedir'], "log")
-            self.addHandler(FileLogTarget(logfile))
+            if logfile:
+                self.addHandler(FileLogTarget(logfile))
             
     def setMinSeverity(self, minSeverity):
         self.severity = _SEVERITIES.get(minSeverity, 1)
@@ -254,7 +257,6 @@
        this process next receives a SIGHUP."""
     resetHooks.append(fn)
 
-
 def onTerminate(fn):
     """Given a 0-argument function fn, cause fn to be invoked when
        this process next receives a SIGTERM."""
@@ -289,7 +291,6 @@
     #outcome, core, sig = status & 0xff00, status & 0x0080, status & 0x7f
     # FFFF Log if outcome wasn't as expected.
 
-
 def _sigHandler(signal_num, _):
     '''(Signal handler for SIGTERM and SIGHUP)'''
     signal.signal(signal_num, _sigHandler)
@@ -300,8 +301,7 @@
     else:
         for hook in resetHooks:
             hook()
-
-
+            
 def installSignalHandlers(child=1,hup=1,term=1):
     '''Register signal handlers for this process.  If 'child', registers
        a handler for SIGCHLD.  If 'hup', registers a handler for SIGHUP.

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- Config.py	25 Jul 2002 15:52:57 -0000	1.3
+++ Config.py	26 Jul 2002 15:47:20 -0000	1.4
@@ -34,8 +34,9 @@
       value5 value5 value5
    """
 
-__all__ = [ 'getConfig', 'loadConfig' ]
+__all__ = [ 'getConfig', 'loadConfig', 'addHook' ]
 
+import os
 import re
 from cStringIO import StringIO
 
@@ -44,128 +45,155 @@
 import mixminion.Packet
 
 #----------------------------------------------------------------------
+
+# global variable to hold the configuration object for this process.
 _theConfiguration = None
 
-def loadConfig(fname=0,server=0):
-    """XXXX"""
+def loadConfig(fname,server=0):
+    """Load the configuration file for this process.  Takes a
+       filename, and a flag to determine whether we're running as a
+       client or a server.
+
+       Registers the configuration object to be reloaded on SIGHUP."""
     global _theConfiguration
     assert _theConfiguration is None
 
     if server:
         _theConfiguration = ServerConfig(fname)
     else:
+        assert fname is not None
         _theConfiguration = ClientConfig(fname)
 
+    mixminion.Common.onReset(_theConfiguration.reload)
+
 def getConfig():
-    """XXXX"""
+    """Return the configuration object for this process, or None if we haven't
+       been configured yet."""
     return _theConfiguration
 #----------------------------------------------------------------------
-#XXXX
+
 _CONFIG_HOOKS = []
 def addHook(hook):
-    'xxxx'
+    '''Add 'hook' (a 0-argument function) to the list of configuration
+       hooks.  Whenever the configuration file is reloaded (as on a
+       SIGHUP), it invokes each of the configuration hooks in the
+       order it was added to the list.'''
+    # This isn't a method of _Config, since we want to be able to call
+    # it before we read the configuration file.
     _CONFIG_HOOKS.append(hook)
     
 #----------------------------------------------------------------------
 
-# Regular expression to match a section headerr.
-_section_re = re.compile(r'\[([^\]]+)\]')
-
-# Regular expression to match the first line of an entry
-_entry_re = re.compile(r'([^:= \t]+)(?:\s*[:=]|[ \t])\s*(.*)')
-
 class ConfigError(MixError):
     """Thrown when an error is found in a configuration file."""
     pass
 
-def _parseBoolean(boolean, validate=0):
+def _parseBoolean(boolean):
+    """Entry validation function.  Converts a config value to a boolean.
+       Raises ConfigError on failure."""
     s = boolean.strip().lower()
     if s in ("1", "yes", "y", "true", "on"):
         return 1
-    elif validate and s not in ("0", "no", "n", "false", "off"):
+    elif s not in ("0", "no", "n", "false", "off"):
         raise ConfigError("Invalid boolean %r" % (boolean))
     else:
         return 0
 
-def _parseSeverity(severity, validate=0):
-    s = boolean.strip().upper()
-    if validate and not mixminion.Common._SEVERITIES.has_key(s):
+def _parseSeverity(severity):
+    """Validation function.  Converts a config value to a log severity.
+       Raises ConfigError on failure."""  
+    s = severity.strip().upper()
+    if not mixminion.Common._SEVERITIES.has_key(s):
         raise ConfigError("Invalid log level %r" % (severity))
     return s
 
-def _parseServerMode(mode, validate=0):
+def _parseServerMode(mode):
+    """Validation function.  Converts a config value to a server mode
+       (one of 'relay' or 'local'). Raises ConfigError on failure."""  
     s = mode.strip().lower()
-    if validate and mode not in ('relay', 'local'):
+    if s not in ('relay', 'local'):
         raise ConfigError("Server mode must be 'Relay' or 'Local'")
     return s
 
+# re to match strings of the form '9 seconds', '1 month', etc.
 _interval_re = re.compile(r'''(\d+\.?\d*|\.\d+)\s+
-                              (second|minute|hour|day|week|month|year)s?''',
+                     (sec|second|min|minute|hour|day|week|mon|month|year)s?''',
                           re.X)
 _seconds_per_unit = {
     'second': 1,
+    'sec':    1,
     'minute': 60,
+    'min':    60,
     'hour':   60*60,
     'day':    60*60*24,
     'week':   60*60*24*7,
-    'month':  60*60*24*30,    # These aren't quite right, but we don't need
-    'year':   60*60*24*365,   # exactness.
+    'mon':    60*60*24*30,
+    'month':  60*60*24*30,    # These last two aren't quite right, but we 
+    'year':   60*60*24*365,   # don't need exactness.
     }
-def _parseInterval(interval, validate=0):
+_abbrev_units = { 'sec' : 'second', 'min': 'minute', 'mon': 'month' }
+def _parseInterval(interval):
+    """Validation function.  Converts a config value to an interval of time,
+       in the format (number of units, name of unit, total number of seconds).
+       Raises ConfigError on failure."""  
     inter = interval.strip().lower()
     m = _interval_re.match(inter)
     if not m:
         raise ConfigError("Unrecognized interval %r" % inter)
     num, unit = float(m.group(1)), m.group(2)
+    unit = _abbrev_units.get(unit, unit)
     nsec = num * _seconds_per_unit[unit]
     return num, unit, nsec
 
-def _parseInt(integer, validate=0):
+def _parseInt(integer):
+    """Validation function.  Converts a config value to an int.
+       Raises ConfigError on failure."""  
     i = integer.strip().lower()
     try:
         return int(i)
     except ValueError, e:
-        raise ConfigError("Unrecongized integer %r" % (integer))
+        raise ConfigError("Expected an integer but got %r" % (integer))
 
-def _parseIP(ip, validate=0):
+def _parseIP(ip):
+    """Validation function.  Converts a config value to an IP address.
+       Raises ConfigError on failure."""  
     i = ip.strip().lower()
-    if validate:
-        try:
-            f = mixminion.Packet._packIP(i)
-        except mixminion.Packet.ParseError, p:
-            raise ConfigError("Invalid IP %r" % i)
+    try:
+        f = mixminion.Packet._packIP(i)
+    except mixminion.Packet.ParseError, p:
+        raise ConfigError("Invalid IP %r" % i)
 
     return i
 
-def _parseCommand(command, validate=0):
+def _parseCommand(command):
+    """Validation function.  Converts a config value to a shell command of
+       the form (fname, optionslist). Raises ConfigError on failure."""  
     c = command.strip().lower().split()
     if not c:
         raise ConfigError("Invalid command %r" %command)
     cmd, opts = c[0], c[1:]
-    if not os.path.exists(cmd) and not os.path.isabs(cmd):
-        for p in os.environ.get('PATH', "").split(os.pathsep):
+    if os.path.isabs(cmd):
+        if not os.path.exists(cmd):
+            raise ConfigError("File not found: %s" % cmd)
+        else:
+            return cmd, opts
+    else:
+        # Path is relative
+        for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
             p = os.path.expanduser(p)
             c = os.path.join(p, cmd)
             if os.path.exists(c):
-                cmd = c
-                break
-        else:
-            raise ConfigError("No match found for command %r" %cmd)
-    return cmd, opts
+                return c, opts
+            
+        raise ConfigError("No match found for command %r" %cmd)
 
-def _parseDir(directory, validate=0):
-    d = directory.strip().lower()
-    if not os.path.exists(d):
-        getLog().warn("Trying to create directory %r"%d)
-        try:
-            os.mkdir(d, 0700)
-        except OSError:
-            raise ConfigError("Couldn't create directory %r"%directory)
-    elif not os.path.isdir(d):
-        raise ConfigError("File %r is not a directory"%directory)
-    return d
+#----------------------------------------------------------------------
 
-def _parseLine(line):
+# Regular expression to match a section header.
+_section_re = re.compile(r'\[([^\]]+)\]')
+# Regular expression to match the first line of an entry
+_entry_re = re.compile(r'([^:= \t]+)(?:\s*[:=]|[ \t])\s*(.*)')
+def _readConfigLine(line):
     """Helper function.  Given a line of a configuration file, return
        a (TYPE, VALUE) pair, where TYPE is one of the following:
 
@@ -197,7 +225,7 @@
             return "ERR", "Bad entry"
         return "ENT", (m.group(1), m.group(2))
 
-def _parseFile(file):
+def _readConfigFile(file):
     """Helper function. Given an open file object for a configuration
        file, parse it into sections.
 
@@ -212,8 +240,7 @@
     lastKey = None
     for line in file.readlines():
         lineno += 1
-        x = _parseLine(line)
-        type, val = _parseLine(line)
+        type, val = _readConfigLine(line)
         if type == 'ERR':
             raise ConfigError("%s at line %s" % (val, lineno))
         elif type == 'SEC':
@@ -238,13 +265,12 @@
        indented by 'ind' spaces.
     """
     ind = " "*ind
-    if len(val)+len(key)+2 <= 79:
+    if len(str(val))+len(key)+2 <= 79:
         return "%s: %s\n" % (key,val)
 
     lines = [ "%s: " %key ]
     #XXXX Bad implementation.
     for v in val.split(" "):
-        print v
         if len(lines[-1])+1+len(v) <= w:
             lines[-1] = "%s %s" % (lines[-1],v)
         else:
@@ -260,19 +286,24 @@
     #  _sections: A map from secname->key->value.
     #  _sectionEntries: A  map from secname->[ (key, value) ] inorder.
     #  _sectionNames: An inorder list of secnames.
-    #  hooks: list of callback functions.
     #
     # Set by a subclass:
     #     _syntax is map from sec->{key:
     #                               (ALLOW/REQUIRE/ALLOW*/REQUIRE*,
     #                                 parseFn,
     #                                 default, ) }
-
+    
+    ## Validation rules:
     # A key without a corresponding entry in _syntax gives an error.
     # A section without a corresponding entry is ignored.
     # ALLOW* and REQUIRE* permit multiple entries with for a given key:
     #   these entries are read into a list.
     # The magic key __SECTION__ describes whether a section is requried.
+    # If parseFn is not None, it is invoked on the entry in order to
+    #   get a value.  Otherwise, the value is string value of the entry.
+    # If the entry is (permissibly) absent, and default is set, then
+    #   the entry's value will be set to default.  Otherwise, the value
+    #   will be set to None.
 
     def __init__(self, fname=None, string=None):
         """Create a new _ConfigFile.  If fname is set, read from
@@ -312,7 +343,7 @@
 
     def __reload(self, file):
         """As in .reload(), but takes an open file object."""
-        sections = _parseFile(file)
+        sections = _readConfigFile(file)
 
         # These will become self.(_sections,_sectionEntries,_sectionNames)
         # if we are successful.
@@ -339,22 +370,27 @@
             if not secConfig:
                 getLog().warn("Skipping unrecognized section %s", secName)
                 continue
-                
+
+            # Set entries from the section, searching for bad entries
+            # as we go.
             for k,v,line in secEntries:
-                sectionEntries.append( (k,v) )
-                entryLines.append(line)
                 rule, parseFn, default = secConfig.get(k, (None,None,None))
                 if not rule:
                     raise ConfigError("Unrecognized key %s on line %s" %
                                       (k, line))
 
+                # Parse and validate the value of this entry.
                 if parseFn is not None:
                     try:
-                        v = parseFn(v, validate=1)
+                        v = parseFn(v)
                     except ConfigError, e:
                         e.args = ("%s at line %s" %(e.args[0],line))
                         raise e
 
+                sectionEntries.append( (k,v) )
+                entryLines.append(line)
+
+                # Insert the entry, checking for impermissible duplicates.
                 if rule in ('REQUIRE*','ALLOW*'):
                     if section.has_key(k):
                         section[k].append(v)
@@ -368,6 +404,8 @@
                     else:
                         section[k] = v
 
+            # Check for missing entries, setting defaults and detecting
+            # missing requirements as we go.
             for k, (rule, parseFn, default) in secConfig.items():
                 if k == '__SECTION__':
                     continue
@@ -375,16 +413,18 @@
                     raise ConfigError("Missing entry %s from section %s"
                                       % (k, secName))
                 elif not section.has_key(k):
-                    if parseFn is None:
-                        section[k] = default
-                    elif default is None:
-                        section[k] = default
+                    if parseFn is None or default is None:
+                        if rule == 'ALLOW*':
+                            section[k] = []
+                        else:
+                            section[k] = default
                     elif rule == 'ALLOW':
                         section[k] = parseFn(default)
                     else:
                         assert rule == 'ALLOW*'
                         section[k] = map(parseFn,default)
 
+        # Check for missing required sections.
         for secName, secConfig in self._syntax.items():
             secRule = secConfig.get('__SECTION__', ('ALLOW',None,None))
             if (secRule[0] == 'REQUIRE'
@@ -394,10 +434,12 @@
                 self_sections[secName] = {}
                 self_sectionEntries[secName] = {}
 
+        # Make sure that sectionEntries is correct (sanity check)
         for s in self_sectionNames:
             for k,v in self_sectionEntries[s]:
                 assert v == self_sections[s][k] or v in self_sections[s][k]
 
+        # Call our validation hook.
         self.validate(self_sections, self_sectionEntries, sectionEntryLines)
 
         self._sections = self_sections
@@ -417,12 +459,12 @@
         return self._sections[sec]
 
     def has_section(self, sec):
-        'XXXX'
+        """Return true if this config object allows a section named 'sec'."""
         return self._sections.has_key(sec)
 
     def getSectionItems(self, sec):
         """Return a list of ordered (key,value) tuples for a given section.
-           section was absent, return an empty map."""
+           If the section was absent, return an empty map."""
         return self._sectionEntries[sec]
 
     def __str__(self):
@@ -437,14 +479,6 @@
             
         return "".join(lines)
 
-_interval_re = re.compile('(\d+\.?\d*|\.\d+)\s+(second|minute|hour)s?')
-def validateInterval(s):
-    m = _interval_re.match(s)
-    if not m:
-        return "ERR", "Invalid interval: %r" %s
-    
-    return "OK", (float(m.group(1)), m.group(2), )
-
 class ClientConfig(_ConfigFile):
     _syntax = {
         'Host' : { '__SECTION__' : ('REQUIRE', None, None),
@@ -455,7 +489,7 @@
                    { '__SECTION__' : ('REQUIRE', None, None),
                      'ServerURL' : ('ALLOW*', None, None),
                      'MaxSkew' : ('ALLOW', _parseInterval, "10 minutes") },
-        'User' : { 'UserDir' : ('ALLOW', _parseDir, "~/.mixminion" ) },
+        'User' : { 'UserDir' : ('ALLOW', None, "~/.mixminion" ) },
         'Security' : { 'PathLength' : ('ALLOW', _parseInt, "8"),
                        'SURBAddress' : ('ALLOW', None, None),
                        'SURBPathLength' : ('ALLOW', None, "8") },
@@ -467,38 +501,11 @@
         #XXXX Write this
         pass
 
-    def getShredCommand(self):
-        return self['Host'].get('ShredCommand', None)
-
-    def getEntropySource(self):
-        return self['Host'].get('EntropySource', None)
-
-    def getDirectoryServerURLs(self):
-        return self['DirectoryServers'].get('ServerURL', [])
-
-    def getMaxSkew(self):
-        # returns seconds.
-        skew = self['DirectoryServers'].get('MaxSkew', "10 minutes")
-        _, _, nsec = _parseInterval(skew)
-        return nsec
-
-    def getUserDir(self):
-        return self['UserDir'].get('UserDir', '.minion')
-
-    def getPathLength(self):
-        return int(self['Security'].get('PathLength', '8'))
-
-    def getSURBPathLength(self):
-        return int(self['Security'].get('SURBPathLength', '8'))
-
-    def getSURBAddress(self):
-        return self['Security'].get('SURBAddress', None)
-
 class ServerConfig(_ConfigFile):
     _syntax = {
         'Host' : ClientConfig._syntax['Host'], 
         'Server' : { '__SECTION__' : ('REQUIRE', None, None),
-                     'Homedir' : ('ALLOW', _parseDir, "/var/spool/minion"),
+                     'Homedir' : ('ALLOW', None, "/var/spool/minion"),
                      'LogFile' : ('ALLOW', None, None),
                      'LogLevel' : ('ALLOW', _parseSeverity, "WARN"),
                      'EchoMessages' : ('ALLOW', _parseBoolean, "no"),
@@ -524,8 +531,8 @@
                             'AddressFile' : ('REQUIRE', None, None),
                             'Command' : ('ALLOW', _parseCommand, "sendmail") },
         }
-    # Missing: Queue-Size / config options
-    #          timeout
+    # XXXX Missing: Queue-Size / Queue config options
+    # XXXX         timeout options
     def __init__(self, fname=None, string=None):
         _ConfigFile.__init__(self, fname, string)
 
@@ -533,83 +540,6 @@
         #XXXX write this.
         pass
     
-    def getShredCommand(self):
-        return self['Host'].get('ShredCommand', None)
-
-    def getEntropySource(self):
-        return self['Host'].get('EntropySource', None)
-
-    def getHomeDir(self):
-        return self['Server'].get('Homedir', "/var/spool/minion")
-        
-    def getLogLevel(self):
-        return _parseSeverity(self['Server'].get('LogLevel', "WARN"))
-
-    def getLogFile(self):
-        return self['Server'].get('LogFile', None)
-
-    def getEchoMessages(self):
-        return _parseBoolean(self['Server'].get('EchoMessages', "no"))
-
-    def getEncryptIdentityKey(self):
-        return _parseBoolean(self['Server'].get('EncryptIdentityKey', "yes"))
-
-    def getMode(self):
-        return _parseServerMode(self['Server'].get('Mode', "local"))
-    
-    def getPublicKeyLifetime(self):
-        _, _, nsec =_parseInterval(self['Server'].get('PublicKeyLifetime',
-                                                           '1 month'))
-        return nsec
-
-    def getEncryptPublicKey(self):
-        return _parseBoolean(self['Server'].get('EncryptPublicKey', "no"))
-
-    def getPublish(self):
-        return _parseBoolean(self['DirectoryServers'].get('Publish', "no"))
-
-    def getDirectoryServerURLs(self):
-        return self['DirectoryServers'].get('ServerURL', [])
-
-    def getMaxSkew(self):
-        # returns seconds.
-        skew = self['DirectoryServers'].get('MaxSkew', "10 minutes")
-        _, _, nsec = _parseInterval(skew)
-        return nsec
-
-    def getIncomingMMTPEnabled(self):
-        return _parseBoolean(self['Incoming/MMTP'].get('Enabled', 'no'))
-
-    def getIncomingMMTP_IP(self):
-        return _parseIP(self['Incoming/MMTP'].get('IP', None))
-
-    def getIncomingMMTP_Port(self):
-        return _parseInt(self['Incoming/MMTP'].get('Port', None))
-
-    def getIncomingMMTP_Rules(self):
-        #XXXX WRITE ME
-        pass
-
-    def getOutgoingMMTPEnabled(self):
-        return _parseBoolean(self['Outgoing/MMTP'].get('Enabled', 'no'))
-
-    def getOutgoingMMTPRules(self):
-        #XXXX WRITE ME
-        pass
-
-    def getMBoxEnabled(self):
-        #XXXX WRITE ME
-        pass
-
-    def getMBoxEnabled(self):
-        return _parseBoolean(self['Delivery/MBox'].get('Enabled', 'no'))
-
-    def getMBoxAddressFile(self):
-        return self['Delivery/MBox'].get('AddressFile', None)
-
-    def getMBoxCommand(self):
-        return self['Delivery/MBox'].get('Command', None)
-
 ## _serverDescriptorSyntax = {
 ##     'Server' : { 'Descriptor-Version' : 'REQUIRE',
 ##                  'IP' : 'REQUIRE',
@@ -629,5 +559,3 @@
 ##                        'Allow' : 'ALLOW*',
 ##                        'Deny' : 'ALLOW*' }
 ##     }
-
-

Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.8
retrieving revision 1.9
diff -u -d -r1.8 -r1.9
--- Crypto.py	25 Jun 2002 11:41:08 -0000	1.8
+++ Crypto.py	26 Jul 2002 15:47:20 -0000	1.9
@@ -8,10 +8,13 @@
    the functions in mixminion.Crypto, and not call _minionlib's crypto
    functionality themselves."""
 
+import os
+import stat
 from types import StringType
 
+import mixminion.Config
 import mixminion._minionlib as _ml
-from mixminion.Common import MixError, MixFatalError, floorDiv, ceilDiv
+from mixminion.Common import MixError, MixFatalError, floorDiv, ceilDiv, getLog
 
 __all__ = [ 'CryptoError', 'init_crypto', 'sha1', 'ctr_crypt', 'prng',
             'strxor', 'lioness_encrypt', 'lioness_decrypt', 'trng',
@@ -32,12 +35,12 @@
 
 def init_crypto():
     """Initialize the crypto subsystem."""
+    trng(1)
     try:
-        # Try to read /dev/urandom.
+        # Try to read /dev/urandom
         trng(1)
     except:
-        raise MixFatalError("Couldn't initialize entropy source "
-                            "(/dev/urandom)")
+        raise MixFatalError("Couldn't initialize entropy source")
     openssl_seed(40)
 
 def sha1(s):
@@ -406,9 +409,40 @@
         _theSharedPRNG = AESCounterPRNG()
     return _theSharedPRNG
 
+_TRNG_FILENAME = None
+def _trng_set_filename():
+    global _TRNG_FILENAME
+    config = mixminion.Config.getConfig()
+    if config is not None:
+        file = config['Host'].get('EntropySource', "/dev/urandom")
+    else:
+        file = "/dev/urandom"
+
+    if not os.path.exists(file):
+        getLog().error("No such file as %s", file)
+        file = None
+    else:
+        st = os.stat(file)
+        if not (st.st_mode & stat.S_IFCHR):
+            getLog().error("Entropy source %s isn't a character device", file)
+            file = None
+
+    if file is None and _TRNG_FILENAME is None:
+        getLog().fatal("No entropy source available")
+        raise MixFatalError("No entropy source available")
+    elif file is None:
+        getLog().warn("Falling back to previous entropy source %s",
+                      _TRNG_FILENAME)
+    else:
+        _TRNG_FILENAME = file
+    
 def _trng_uncached(n):
     '''Underlying access to our true entropy source.'''
-    f = open('/dev/urandom')
+    if _TRNG_FILENAME is None:
+        _trng_set_filename()
+        mixminion.Config.addHook(_trng_set_filename)
+        
+    f = open(_TRNG_FILENAME)
     d = f.read(n)
     f.close()
     return d

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.12
retrieving revision 1.13
diff -u -d -r1.12 -r1.13
--- test.py	25 Jul 2002 15:52:57 -0000	1.12
+++ test.py	26 Jul 2002 15:47:20 -0000	1.13
@@ -65,6 +65,9 @@
 
 atexit.register(try_unlink, _unlink_on_exit_list)    
 
+def floatEq(f1,f2):
+    return abs(f1-f2) < .00001
+
 #----------------------------------------------------------------------
 import mixminion._minionlib as _ml
 
@@ -1486,16 +1489,24 @@
 #----------------------------------------------------------------------
 # Config files
 
-from mixminion.Config import _ConfigFile, ConfigError
+from mixminion.Config import _ConfigFile, ConfigError, _parseInt
 
 class TestConfigFile(_ConfigFile):
     _syntax = { 'Sec1' : {'__SECTION__': ('REQUIRE', None, None),
                           'Foo': ('REQUIRE', None, None),
-                          'Bar': ('ALLOW', None, None),
+                          'Bar': ('ALLOW', None, "default"),
                           'Baz': ('ALLOW', None, None),},
                 'Sec2' : {'Fob': ('ALLOW*', None, None),
                           'Bap': ('REQUIRE', None, None),
-                          'Quz': ('REQUIRE*', None, None), }
+                          'Quz': ('REQUIRE*', None, None), },
+                'Sec3' : {'IntAS': ('ALLOW', _parseInt, None),
+                          'IntAS2': ('ALLOW', _parseInt, None),
+                          'IntASD': ('ALLOW', _parseInt, "5"),
+                          'IntASD2': ('ALLOW', _parseInt, "5"),
+                          'IntAM': ('ALLOW*', _parseInt, None),
+                          'IntAMD': ('ALLOW*', _parseInt, ["5", "2"]),
+                          'IntAMD2': ('ALLOW*', _parseInt, ["5", "2"]),
+                          'IntRS': ('REQUIRE', _parseInt, None) }
                 }
     def __init__(self, fname=None, string=None):
         _ConfigFile.__init__(self,fname,string)
@@ -1530,6 +1541,13 @@
 Quz : 88
      88
 
+[Sec3]
+IntAS=9
+IntASD=10
+IntAMD=8
+IntAMD=10
+IntRS=5
+
   """
         
         f = TCF(string=longerString)
@@ -1548,7 +1566,8 @@
         self.assertEquals(str(f),
            ("[Sec1]\nFoo: abcde f\nBar: bar\nBaz:  baz and more baz"+
             " and more baz\n\n[Sec2]\nBap: +\nQuz: 99 99\nFob: 1\n"+
-            "Quz: 88 88\n\n"))
+            "Quz: 88 88\n\n[Sec3]\nIntAS: 9\nIntASD: 10\nIntAMD: 8\n"+
+            "IntAMD: 10\nIntRS: 5\n\n"))
         # Test file input
         fn = tempfile.mktemp()
         unlink_on_exit(fn)
@@ -1560,6 +1579,15 @@
         self.assertEquals(f['Sec1']['Bar'], 'bar')
         self.assertEquals(f['Sec2']['Quz'], ['99 99', '88 88'])
 
+        self.assertEquals(f['Sec3']['IntAS'], 9)
+        self.assertEquals(f['Sec3']['IntAS2'], None)
+        self.assertEquals(f['Sec3']['IntASD'], 10)
+        self.assertEquals(f['Sec3']['IntASD2'], 5)
+        self.assertEquals(f['Sec3']['IntAM'], [])
+        self.assertEquals(f['Sec3']['IntAMD'], [8,10])
+        self.assertEquals(f['Sec3']['IntAMD2'], [5,2])
+        self.assertEquals(f['Sec3']['IntRS'], 5)
+
         # Test failing reload
         file = open(fn, 'w')
         file.write("[Sec1]\nFoo=99\nBadEntry 3\n\n")
@@ -1576,9 +1604,10 @@
         file.close()
         f.reload()
         self.assertEquals(f['Sec1']['Foo'], 'a')
-        self.assertEquals(f['Sec1'].get('Bar', None), None)
+        self.assertEquals(f['Sec1']['Bar'], "default")
         self.assertEquals(f['Sec2'], {})
 
+
     def testBadFiles(self):
         TCF = TestConfigFile
         def fails(string, self=self):
@@ -1593,7 +1622,66 @@
         fails("[Sec1]\nBaz: 3\n") # Missing key
         fails("[Sec2]\nBap = 9\nQuz=6\n") # Missing section
         fails("[Sec1]\nFoo 1\n[Sec2]\nBap = 9\n") # Missing require*
+        fails("[Sec1]\nFoo: Bar\n[Sec3]\nIntRS=Z\n") # Failed validation
+
+    def testValidationFns(self):
+        import mixminion.Config as C
+
+        self.assertEquals(C._parseBoolean("yes"), 1)
+        self.assertEquals(C._parseBoolean(" NO"), 0)
+        self.assertEquals(C._parseSeverity("error"), "ERROR")
+        self.assertEquals(C._parseServerMode(" relay "), "relay")
+        self.assertEquals(C._parseServerMode("Local"), "local")
+        self.assertEquals(C._parseInterval(" 1 sec "), (1,"second", 1))
+        self.assertEquals(C._parseInterval(" 99 sec "), (99,"second", 99))
+        self.failUnless(floatEq(C._parseInterval("1.5 minutes")[2],
+                                90))
+        self.assertEquals(C._parseInterval("2 houRS"), (2,"hour",7200))
+        self.assertEquals(C._parseInt("99"), 99)
+        self.assertEquals(C._parseIP("192.168.0.1"), "192.168.0.1")
+        # XXXX Won't work on Windows.
+        self.assertEquals(C._parseCommand("ls -l"), ("/bin/ls", ['-l']))
+        self.assertEquals(C._parseCommand("rm"), ("/bin/rm", []))
+        self.assertEquals(C._parseCommand("/bin/ls"), ("/bin/ls", []))
+        self.failUnless(C._parseCommand("python")[0] is not None)
+        
+        def fails(fn, val, self=self):
+            self.failUnlessRaises(ConfigError, fn, val)
 
+        fails(C._parseBoolean, "yo")
+        fails(C._parseBoolean, "'yo'")
+        fails(C._parseBoolean, "")
+        fails(C._parseSeverity, "really bad")
+        fails(C._parseServerMode, "whatever")
+        fails(C._parseInterval, "seconds")
+        fails(C._parseInterval, "15")
+        fails(C._parseInterval, " 10 intervals")
+        fails(C._parseInt, "9.9")
+        fails(C._parseInt, "9abc")
+        fails(C._parseIP, "256.0.0.1")
+        fails(C._parseIP, "192.0.0")
+        fails(C._parseIP, "192.0.0.0.0")
+        fails(C._parseIP, "A.0.0.0")
+        nonexistcmd = '/file/that/does/not/exist'
+        if not os.path.exists(nonexistcmd):
+            fails(C._parseCommand, nonexistcmd)
+        else:
+            print 'Whoa. Kurt Go"del would be proud of you.'
+
+        # Nobody would ever have an executable named after my sister's
+        # cats, would they?
+        nonexistcmd = 'LindenAndPierre -meow'
+        try:
+            cmd, opts = C._parseCommand(nonexistcmd)
+            if os.path.exists(cmd):
+                # Ok, I guess they would.
+                self.failUnlessEquals(opts, ["-meow"])
+            else:
+                self.fail("_parseCommand is not working as expected")
+        except ConfigError, e:
+            # This is what we expect
+            pass
+  
 def testSuite():
     suite = unittest.TestSuite()
     loader = unittest.TestLoader()