[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[minion-cvs] Start improve list-servers; refactor config types; less...



Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv20799/lib/mixminion

Modified Files:
	ClientDirectory.py ClientMain.py ClientUtils.py Config.py 
	MMTPClient.py ServerInfo.py test.py testSupport.py 
Log Message:
Start improve list-servers; refactor config types; less mem usage for flush.

ClientDirectory,ClientMain:
 - New interface to display server information.  Not done yet, but
   better than groveling over directories by hand.

ClientMain, MMTPClient, ClientUtils:
 - Change client queue flushing code so that we don't load everything
   into memory when we flush the queue.  Moved ClientQueue to use a
   metadata store; added proxy class to lazy-load flushable messages
   without changing sendMessages interface.

Config:
 - Add resolveFeature/getFeature functions to generic config file
   interface to make it easier for users to describe aspects of server
   descriptors.

Config, ServerInfo, test, Modules, ServerConfig
 - Change the way that types are specficied for _ConfigFile
   derivitives: now they're by name instead of by by function.  This
   makes unparsing a snap, and keeps us from having to use private
   (underscore-prefixed) names from Config.



Index: ClientDirectory.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientDirectory.py,v
retrieving revision 1.7
retrieving revision 1.8
diff -u -d -r1.7 -r1.8
--- ClientDirectory.py	19 Oct 2003 03:12:01 -0000	1.7
+++ ClientDirectory.py	7 Nov 2003 07:03:28 -0000	1.8
@@ -400,6 +400,44 @@
                 lines.append(line)
         return lines
 
+    def listServers2(self, features, at=None, goodOnly=0):
+        """DOCDOC
+           Returns a dict from nickname to (va,vu) to feature to value."""
+        result = {}
+        if not self.fullServerList:
+            return {}
+        dirFeatures = [ 'status' ]
+        resFeatures = []
+        for f in features:
+            if f.lower() in dirFeatures:
+                resFeatures.append((f, ('+', f.lower())))
+            else:
+                feature = mixminion.Config.resolveFeatureName(
+                    f, mixminion.ServerInfo.ServerInfo)
+                resFeatures.append((f, feature))
+        for sd, _ in self.fullServerList:
+            if at and not sd.isValidAt(at):
+                continue
+            nickname = sd.getNickname()
+            isGood = self.goodServerNicknames.get(nickname, 0)
+            if goodOnly and not isGood:
+                continue
+            va, vu = sd['Server']['Valid-After'], sd['Server']['Valid-Until']
+            d = result.setdefault(nickname, {}).setdefault((va,vu), {})
+            for feature,(sec,ent) in resFeatures:
+                if sec == '+':
+                    if ent == 'status':
+                        if isGood:
+                            d['status'] = "(ok)"
+                        else:
+                            d['status'] = "(not recommended)"
+                    else:
+                        assert 0
+                else:
+                    d[feature] = str(sd.getFeature(sec,ent))
+
+        return result
+
     def __find(self, lst, startAt, endAt):
         """Helper method.  Given a list of (ServerInfo, where), return all
            elements that are valid for all time between startAt and endAt.
@@ -755,6 +793,87 @@
         else:
             LOG.warn("This software is newer than any version "
                      "on the recommended list.")
+
+#----------------------------------------------------------------------
+def compressServerList(featureMap, ignoreGaps=0, terse=0):
+    """DOCDOC
+        featureMap is nickname -> va,vu -> feature -> value .
+        result is  same format, but time is compressed.
+    """
+    result = {}
+    for nickname in featureMap.keys():
+        byStartTime = featureMap[nickname].items()
+        byStartTime.sort()
+        r = []
+        for (va,vu),features in byStartTime:
+            if not r:
+                r.append((va,vu,features))
+                continue
+            lastva, lastvu, lastfeatures = r[-1]
+            if (ignoreGaps or lastvu == va) and lastfeatures == features:
+                r[-1] = lastva, vu, features
+            else:
+                r.append((va,vu,features))
+        result[nickname] = {}
+        for va,vu,features in r:
+            result[nickname][(va,vu)] = features
+
+        if not terse: continue
+        if not result[nickname]: continue
+        
+        ritems = result[nickname].items()
+        minva = min([ va for (va,vu),features in ritems ])
+        maxvu = max([ vu for (va,vu),features in ritems ])
+        rfeatures = {}
+        for (va,vu),features in ritems:
+            for f,val in features.items():
+                if rfeatures.setdefault(f,val) != val:
+                    rfeatures[f] += " / %s"%val
+        result[nickname] = { (minva,maxvu) : rfeatures }
+    
+    return result
+
+def formatFeatureMap(features, featureMap, showTime=0, cascade=0, sep=" "):
+    # No cascade:
+    # nickname:time1: value value value
+    # nickname:time2: value value value
+
+    # Cascade=1:
+    # nickname:
+    #     time1: value value value
+    #     time2: value value value
+
+    # Cascade = 2:
+    # nickname:
+    #     time:
+    #       feature:value
+    #       feature:value
+    #       feature:value
+    nicknames = [ (nn.lower(), nn) for nn in featureMap.keys() ]
+    nicknames.sort()
+    lines = []
+    if not nicknames: return lines
+    for _, nickname in nicknames:
+        d = featureMap[nickname]
+        if not d: continue
+        items = d.items()
+        items.sort()
+        if cascade: lines.append("%s:"%nickname)
+        for (va,vu),fmap in items:
+            ftime = "%s to %s"%(formatDate(va),formatDate(vu))
+            if cascade and showTime:
+                lines.append("  %s:"%ftime)
+            if cascade:
+                for f in features:
+                    v = fmap[f]
+                    lines.append("    %s:%s"%(f,v))
+            elif showTime:
+                lines.append("%s:%s:%s" %(nickname,ftime,
+                   sep.join([fmap[f] for f in features])))
+            else:
+                lines.append("%s:%s" %(nickname,
+                   sep.join([fmap[f] for f in features])))
+    return lines
 
 #----------------------------------------------------------------------
 

Index: ClientMain.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientMain.py,v
retrieving revision 1.123
retrieving revision 1.124
diff -u -d -r1.123 -r1.124
--- ClientMain.py	19 Oct 2003 03:41:45 -0000	1.123
+++ ClientMain.py	7 Nov 2003 07:03:28 -0000	1.124
@@ -444,8 +444,16 @@
         """
         #XXXX write unit tests
 
+        class MessageProxy:
+            def __init__(self,h,queue):
+                self.h = h
+                self.queue = queue
+            def __str__(self):
+                return self.queue.getPacket(self.h)[0]
+            def __cmp__(self,other):
+                return cmp(id(self),id(other))
+
         LOG.info("Flushing message queue")
-        # XXXX This is inefficient in space!
         clientLock()
         try:
             handles = self.queue.getHandles()
@@ -457,9 +465,10 @@
             messagesByServer = {}
             for h in handles:
                 try:
-                    message, routing, _ = self.queue.getPacket(h)
+                    routing = self.queue.getRoutingt(h)
                 except mixminion.Filestore.CorruptedFile: 
                     continue
+                message = MessageProxy(h,self.queue)
                 messagesByServer.setdefault(routing, []).append((message, h))
         finally:
             clientUnlock()
@@ -512,7 +521,7 @@
         try:
             clientLock()
             for msg in msgList:
-                h = self.queue.queuePacket(msg, routing)
+                h = self.queue.queuePacket(str(msg), routing)
                 handles.append(h)
         finally:
             clientUnlock()
@@ -1193,7 +1202,13 @@
     parser.init()
     directory = parser.directory
 
-    for line in directory.listServers():
+    #for line in directory.listServers():
+    #    print line
+    features = ["caps", "status", "secure-configuration"]
+    fm = directory.listServers2(features)
+    #fm = mixminion.ClientDirectory.compressServerList(fm)
+    for line in mixminion.ClientDirectory.formatFeatureMap(
+        features,fm,1,cascade=1):
         print line
 
 _UPDATE_SERVERS_USAGE = """\

Index: ClientUtils.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ClientUtils.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -d -r1.3 -r1.4
--- ClientUtils.py	9 Oct 2003 15:26:16 -0000	1.3
+++ ClientUtils.py	7 Nov 2003 07:03:28 -0000	1.4
@@ -324,6 +324,7 @@
     #           )
     # XXXX change this to be OO; add nicknames.
     # XXXX006 write unit tests
+    # XXXX Switch to use metadata.
     def __init__(self, directory, prng=None):
         """Create a new ClientQueue object, storing packets in 'directory'
            and generating random filenames using 'prng'."""
@@ -339,16 +340,19 @@
                 fname_new = os.path.join(directory, "msg_"+handle)
                 os.rename(fname_old, fname_new)
         
-        self.store = mixminion.Filestore.ObjectStore(
+        self.store = mixminion.Filestore.ObjectMetadataStore(
             directory, create=1, scrub=1)
 
+        self.metadataLoaded = 0
+
     def queuePacket(self, message, routing):
         """Insert the 32K packet 'message' (to be delivered to 'routing')
            into the queue.  Return the handle of the newly inserted packet."""
         mixminion.ClientMain.clientLock()
         try:
             fmt = ("PACKET-0", message, routing, previousMidnight(time.time()))
-            return self.store.queueObject(fmt)
+            meta = ("V0", routing, previousMidnight(time.time()))
+            return self.store.queueObjectAndMetadata(fmt,meta)
         finally:
             mixminion.ClientMain.clientUnlock()
 
@@ -361,9 +365,14 @@
         finally:
             mixminion.ClientMain.clientUnlock()
 
+    def getRouting(self, handle):
+        """DOCDOC"""
+        self.loadMetadata()
+        return self.getMetadata(handle)[1]
+
     def getPacket(self, handle):
         """Given a handle, return a 3-tuple of the corresponding
-           32K packet, IPV4Info, and time of first queueing.  (The time
+           32K packet, {IPV4/Host}Info, and time of first queueing.  (The time
            is rounded down to the closest midnight GMT.)  May raise 
            CorruptedFile."""
         obj = self.store.getObject(handle)
@@ -384,6 +393,7 @@
     def removePacket(self, handle):
         """Remove the packet named with the handle 'handle'."""
         self.store.removeMessage(handle)
+        # XXXX006 This cleanQueue shouldn't need to happen so often!
         self.store.cleanQueue()
 
     def inspectQueue(self, now=None):
@@ -395,10 +405,11 @@
         if not handles:
             print "[Queue is empty.]"
             return
+        self.loadMetadata()
         timesByServer = {}
         for h in handles:
             try:
-                _, routing, when = self.getPacket(h)
+                _, routing, when = self.store.getMetadata(h)
             except mixminion.Filestore.CorruptedFile:
                 continue
             timesByServer.setdefault(routing, []).append(when)
@@ -418,9 +429,10 @@
             now = time.time()
         cutoff = now - maxAge
         remove = []
+        self.loadMetadata()
         for h in self.getHandles():
             try:
-                when = self.getPacket(h)[2]
+                when = self.store.getMetadata(h)[2]
             except mixminion.Filestore.CorruptedFile:
                 continue
             if when < cutoff:
@@ -429,3 +441,16 @@
         for h in remove:
             self.store.removeMessage(h)
         self.store.cleanQueue()
+
+    def loadMetadata(self):
+        """DOCDOC"""
+        if self.metadataLoaded:
+            return
+
+        def fixupHandle(h,self=self):
+            packet, routing, when = self.getPacket(h)
+            return "V0", routing, when
+
+        self.store.loadAllMetadata(fixupHandle)
+
+        self.metadataLoaded = 1

Index: Config.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Config.py,v
retrieving revision 1.61
retrieving revision 1.62
diff -u -d -r1.61 -r1.62
--- Config.py	19 Oct 2003 03:12:02 -0000	1.61
+++ Config.py	7 Nov 2003 07:03:28 -0000	1.62
@@ -68,7 +68,7 @@
 import mixminion.Crypto
 
 from mixminion.Common import MixError, LOG, ceilDiv, englishSequence, \
-   isPrintingAscii, stripSpace, stringContains, UIError
+     formatBase64, isPrintingAscii, stripSpace, stringContains, UIError
 
 class ConfigError(MixError):
     """Thrown when an error is found in a configuration file."""
@@ -166,6 +166,27 @@
             ilist.append(interval)
     return ilist
 
+def _unparseIntervalList(lst):
+    if lst == []:
+        return ""
+    r = [ (lst[0], 1) ]
+    for dur in lst[1:]:
+        if dur == r[-1][0]:
+            r[-1] = (dur, r[-1][1]+1)
+        else:
+            r.append((dur,1))
+    result = []
+    for dur, reps in r:
+        d = mixminion.Common.Duration(dur)
+        t = mixminion.Common.Duration(dur*reps)
+        d.reduce()
+        t.reduce()
+        if reps>1:
+            result.append("every %s for %s"%(d,t))
+        else:
+            result.append(str(d))
+    return ", ".join(result)
+
 def _parseInt(integer):
     """Validation function.  Converts a config value to an int.
        Raises ConfigError on failure."""
@@ -194,6 +215,16 @@
     else:
         return long(val)*unit
 
+def _unparseSize(size):
+    names = ["b", "KB", "MB", "GB"]
+    idx = 0
+    while 1:
+        if (size & 1023) or names[idx] == "GB":
+            return "%s %s"%(size,names[idx])
+        else:
+            idx += 1
+            size >>= 10
+
 # Regular expression to match a dotted quad.
 _ip_re = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
 
@@ -261,7 +292,6 @@
             
     return ip
 
-
 def _parseHost(host):
     """DOCDOC"""
     host = host.strip()
@@ -587,6 +617,46 @@
     lines.append("") # so the last line ends with \n
     return "\n".join(lines)
 
+
+def resolveFeatureName(name, klass):
+    """DOCDOC"""
+    #XXXX006 this should be case insensitive.
+    syn = klass._syntax
+    name = name.lower()
+    if klass._features.has_key(name):
+        return "-", name
+    elif ':' in name:
+        idx = name.index(':')
+        sec, ent = name[:idx], name[idx+1:]
+        goodSection = None
+        for section, entries in syn.items():
+            if section.lower() == sec:
+                goodSection = section
+                for entry in entries.keys():
+                    if entry.lower() == ent:
+                        return section, entry
+        if goodSection:
+            raise UIError("Section %s has no entry %r"%(section,ent))
+        else:
+            raise UIError("No such section as %s"%sec)
+    else:
+        result =  []
+        for secname, secitems in syn.items():
+            if secname.lower() == name:
+                raise UIError("No key given for section %s"%secname)
+            for entname in secitems.keys():
+                if entname.lower() == name:
+                    result.append((secname, entname))
+        if len(result) == 0:
+            raise UIError("No key named %r found"%name)
+        elif len(result) > 1:
+            secs = [ "%s:%s"%(secname,entname) for secname,entname
+                     in result ]
+            raise UIError("%r is ambiguous.  Did you mean %s?",
+                          name, englishSequence(secs,compound="or"))
+        else:
+            return result[0]
+
 class _ConfigFile:
     """Base class to parse, validate, and represent configuration files.
     """
@@ -603,7 +673,7 @@
     # Fields to be set by a subclass:
     #     _syntax is map from sec->{key:
     #                               (ALLOW/REQUIRE/ALLOW*/REQUIRE*,
-    #                                 parseFn,
+    #                                 type,
     #                                 default, ) }
     #     _restrictFormat is 1/0: do we allow full RFC822ness, or do
     #         we insist on a tight data format?
@@ -624,6 +694,30 @@
     #   the entry's value will be set to default.  Otherwise, the value
     #   will be set to None.
 
+    CODING_FNS = {
+        "boolean" :  (_parseBoolean, lambda b: b and "yes" or "no"),
+        "severity" : (_parseSeverity, str),
+        "serverMode"  : (_parseServerMode, str),
+        "interval" : (_parseInterval, str),
+        "intervalList" : (_parseIntervalList, _unparseIntervalList),
+        "int" : (_parseInt, str),
+        "size" : (_parseSize, _unparseSize),
+        "IP" : (_parseIP, str),
+        "IP6" : (_parseIP6, str),
+        "host" : (_parseHost, str),
+        "addressSet_allow" : (_parseAddressSet_allow, str), #XXXX
+        "addressSet_deny" : (_parseAddressSet_deny, str), #XXXX
+        "command" : (_parseCommand, lambda c,o: " ".join([c," ".join(o)])),
+        "base64" : (_parseBase64, mixminion.Common.formatBase64),
+        "hex" : (_parseHex, binascii.b2a_hex),
+        "publicKey" : (_parsePublicKey, lambda r: "<public key>"),
+        "date" : (_parseDate, mixminion.Common.formatDate),
+        "time" : (_parseTime, mixminion.Common.formatTime),
+        "nickname" : (_parseNickname, str),
+        "filename" : (_parseFilename, str),
+        "user" : (_parseUser, str),
+        }
+
     _syntax = None
     _features = {}
     _restrictFormat = 0
@@ -698,7 +792,7 @@
             # as we go.
             for k,v,line in secEntries:
                 try:
-                    rule, parseFn, default = secConfig[k]
+                    rule, parseType, default = secConfig[k]
                 except KeyError:
                     msg = "Unrecognized key %s on line %s"%(k,line)
                     acceptedIn = [ sn for sn,sc in self._syntax.items()
@@ -714,6 +808,8 @@
                         LOG.warn(msg)
                         continue
 
+                parseFn, _ = self.CODING_FNS.get(parseType,(None,None))
+
                 # Parse and validate the value of this entry.
                 if parseFn is not None:
                     try:
@@ -741,7 +837,7 @@
 
             # Check for missing entries, setting defaults and detecting
             # missing requirements as we go.
-            for k, (rule, parseFn, default) in secConfig.items():
+            for k, (rule, parseType, default) in secConfig.items():
                 if k == '__SECTION__':
                     continue
                 elif not section.has_key(k):
@@ -749,6 +845,7 @@
                         raise ConfigError("Missing entry %s from section %s"
                                           % (k, secName))
                     else:
+                        parseFn, _ = self.CODING_FNS.get(parseType,(None,None))
                         if parseFn is None or default is None:
                             if rule == 'ALLOW*':
                                 section[k] = []
@@ -792,40 +889,16 @@
         """
         return contents
 
-    def resolveFeatureName(self, name):
-        """DOCDOC"""
-        #XXXX006 this should be case insensitive.
-        syn = self._syntax
-        if self._features.has_key(name):
-            return "-", name
-        elif ':' in name:
-            idx = name.index(':')
-            sec, ent = name[:idx], name[idx+1:]
-            if not syn.has_key(sec) or not syn[sec].has_key[ent]:
-                raise UIError("Section %s has no entry %s"%(sec,ent))
-            return sec,ent
-        elif syn.has_key(name):
-            raise UIError("No key given for section %s"%name)
-        else:
-            secs = []
-            for secname, secitems in syn.items():
-                if secitems.has_key(name):
-                    secs.append(name)
-            if len(secs) == 0:
-                raise UIError("No key named %s found"%name)
-            elif len(secs) > 1:
-                secs = [ "%s/%s"%(name, sec) for sec in secs ]
-                raise UIError("%s is ambiguous.  Did you mean %s?",
-                              name, englishSequence(secs,compound="or"))
-            else:
-                return secs[0],name
-
     def getFeature(self,sec,name):
         """DOCDOC"""
-        if sec == "-":
-            return "XXXX" #XXXX006 insert magic.
-        else:
-            return self[sec].get(name,"<none>")
+        assert sec not in ("+","-")
+        parseType = self._syntax[sec].get(name)[1]
+        _, unparseFn = self.CODING_FNS[parseType]
+        try:
+            v = self[sec][name]
+        except KeyError:
+            return "<none>"
+        return unparseFn(v)
 
     def validate(self, entryLines, fileContents):
         """Check additional semantic properties of a set of configuration
@@ -872,25 +945,25 @@
     _restrictKeys = _restrictSections = 1
     _syntax = {
         'Host' : { '__SECTION__' : ('ALLOW', None, None),
-                   'ShredCommand': ('ALLOW', _parseCommand, None),
-                   'EntropySource': ('ALLOW', _parseFilename, "/dev/urandom"),
-                   'TrustedUser': ('ALLOW*', _parseUser, None),
-                   'FileParanoia': ('ALLOW', _parseBoolean, "yes"),
+                   'ShredCommand': ('ALLOW', "command", None),
+                   'EntropySource': ('ALLOW', "filename", "/dev/urandom"),
+                   'TrustedUser': ('ALLOW*', "user", None),
+                   'FileParanoia': ('ALLOW', "boolean", "yes"),
                    },
         'DirectoryServers' :
                    { '__SECTION__' : ('REQUIRE', None, None),
                      'ServerURL' : ('ALLOW*', None, None),
-                     'MaxSkew' : ('ALLOW', _parseInterval, "10 minutes") },
-        'User' : { 'UserDir' : ('ALLOW', _parseFilename, "~/.mixminion" ) },
-        'Security' : { 'PathLength' : ('ALLOW', _parseInt, "8"),
+                     'MaxSkew' : ('ALLOW', "interval", "10 minutes") },
+        'User' : { 'UserDir' : ('ALLOW', "filename", "~/.mixminion" ) },
+        'Security' : { 'PathLength' : ('ALLOW', "int", "8"),
                        'SURBAddress' : ('ALLOW', None, None),
-                       'SURBPathLength' : ('ALLOW', _parseInt, "4"),
-                       'SURBLifetime' : ('ALLOW', _parseInterval, "7 days"),
+                       'SURBPathLength' : ('ALLOW', "int", "4"),
+                       'SURBLifetime' : ('ALLOW', "interval", "7 days"),
                        'ForwardPath' : ('ALLOW', None, "*"),
                        'ReplyPath' : ('ALLOW', None, "*"),
                        'SURBPath' : ('ALLOW', None, "*"),
                        },
-        'Network' : { 'ConnectionTimeout' : ('ALLOW', _parseInterval, None) }
+        'Network' : { 'ConnectionTimeout' : ('ALLOW', "interval", None) }
         }
     def __init__(self, fname=None, string=None):
         _ConfigFile.__init__(self, fname, string)

Index: MMTPClient.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/MMTPClient.py,v
retrieving revision 1.40
retrieving revision 1.41
diff -u -d -r1.40 -r1.41
--- MMTPClient.py	20 Oct 2003 18:19:56 -0000	1.40
+++ MMTPClient.py	7 Nov 2003 07:03:28 -0000	1.41
@@ -246,7 +246,7 @@
             elif t == "RENEGOTIATE":
                 con.renegotiate()
             else:
-                con.sendPacket(p)
+                con.sendPacket(str(p))
             if callback is not None:
                 callback(idx)
     finally:

Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.59
retrieving revision 1.60
diff -u -d -r1.59 -r1.60
--- ServerInfo.py	19 Oct 2003 03:12:02 -0000	1.59
+++ ServerInfo.py	7 Nov 2003 07:03:28 -0000	1.60
@@ -53,56 +53,56 @@
     _syntax = {
         "Server" : { "__SECTION__": ("REQUIRE", None, None),
                      "Descriptor-Version": ("REQUIRE", None, None),
-                     "Nickname": ("REQUIRE", C._parseNickname, None),
-                     "Identity": ("REQUIRE", C._parsePublicKey, None),
-                     "Digest": ("REQUIRE", C._parseBase64, None),
-                     "Signature": ("REQUIRE", C._parseBase64, None),
-                     "Published": ("REQUIRE", C._parseTime, None),
-                     "Valid-After": ("REQUIRE", C._parseDate, None),
-                     "Valid-Until": ("REQUIRE", C._parseDate, None),
+                     "Nickname": ("REQUIRE", "nickname", None),
+                     "Identity": ("REQUIRE", "publicKey", None),
+                     "Digest": ("REQUIRE", "base64", None),
+                     "Signature": ("REQUIRE", "base64", None),
+                     "Published": ("REQUIRE", "time", None),
+                     "Valid-After": ("REQUIRE", "date", None),
+                     "Valid-Until": ("REQUIRE", "date", None),
                      "Contact": ("ALLOW", None, None),
                      "Comments": ("ALLOW", None, None),
-                     "Packet-Key": ("REQUIRE", C._parsePublicKey, None),
+                     "Packet-Key": ("REQUIRE", "publicKey", None),
                      "Contact-Fingerprint": ("ALLOW", None, None),
                      # XXXX010 change these next few to "REQUIRE".
                      "Packet-Formats": ("ALLOW", None, None),#XXXX007 remove
                      "Packet-Versions": ("ALLOW", None, None),
                      "Software": ("ALLOW", None, None),
-                     "Secure-Configuration": ("ALLOW", C._parseBoolean, None),
+                     "Secure-Configuration": ("ALLOW", "boolean", None),
                      "Why-Insecure": ("ALLOW", None, None),
                      },
         "Incoming/MMTP" : {
                      "Version": ("REQUIRE", None, None),
-                     "IP": ("ALLOW", C._parseIP, None),#XXXX007 remove
-                     "Hostname": ("ALLOW", C._parseHost, None),#XXXX008 require
-                     "Port": ("REQUIRE", C._parseInt, None),
-                     "Key-Digest": ("ALLOW", C._parseBase64, None),#XXXX007 rmv
+                     "IP": ("ALLOW", "IP", None),#XXXX007 remove
+                     "Hostname": ("ALLOW", "host", None),#XXXX008 require
+                     "Port": ("REQUIRE", "int", None),
+                     "Key-Digest": ("ALLOW", "base64", None),#XXXX007 rmv
                      "Protocols": ("REQUIRE", None, None),
-                     "Allow": ("ALLOW*", C._parseAddressSet_allow, None),
-                     "Deny": ("ALLOW*", C._parseAddressSet_deny, None),
+                     "Allow": ("ALLOW*", "addressSet_allow", None),
+                     "Deny": ("ALLOW*", "addressSet_deny", None),
                      },
         "Outgoing/MMTP" : {
                      "Version": ("REQUIRE", None, None),
                      "Protocols": ("REQUIRE", None, None),
-                     "Allow": ("ALLOW*", C._parseAddressSet_allow, None),
-                     "Deny": ("ALLOW*", C._parseAddressSet_deny, None),
+                     "Allow": ("ALLOW*", "addressSet_allow", None),
+                     "Deny": ("ALLOW*", "addressSet_deny", None),
                      },
         "Delivery/MBOX" : {
                      "Version": ("REQUIRE", None, None),
                      # XXXX006 change to 'REQUIRE'
-                     "Maximum-Size": ("ALLOW", C._parseInt, "32"),
+                     "Maximum-Size": ("ALLOW", "int", "32"),
                      # XXXX006 change to 'REQUIRE'
-                     "Allow-From": ("ALLOW", C._parseBoolean, "yes"),
+                     "Allow-From": ("ALLOW", "boolean", "yes"),
                      },
         "Delivery/SMTP" : {
                      "Version": ("REQUIRE", None, None),
                      # XXXX006 change to 'REQUIRE'
-                     "Maximum-Size": ("ALLOW", C._parseInt, "32"),
-                     "Allow-From": ("ALLOW", C._parseBoolean, "yes"),
+                     "Maximum-Size": ("ALLOW", "int", "32"),
+                     "Allow-From": ("ALLOW", "boolean", "yes"),
                      },
         "Delivery/Fragmented" : {
                      "Version": ("REQUIRE", None, None),
-                     "Maximum-Fragments": ("REQUIRE", C._parseInt, None),
+                     "Maximum-Fragments": ("REQUIRE", "int", None),
                      },
         # We never read these values, except to see whether we should
         # regenerate them.  Depending on these options would violate
@@ -112,6 +112,7 @@
                      "Configuration": ("ALLOW", None, None),
                      },
         }
+    _features = { "capabilities" : 1, "caps" : 1 }
     expected_versions = {
          "Server" : ( "Descriptor-Version", "0.2"),
          "Incoming/MMTP" : ("Version", "0.1"),
@@ -417,6 +418,15 @@
                 valid -= o.getIntervalSet()
         return valid.isEmpty()
 
+    def getFeature(self,sec,name):
+        """DOCDOC"""
+        if sec == '-':
+            if name in ("caps", "capabilities"):
+                return " ".join(self.getCaps())
+            assert 0
+        else:
+            return mixminion.Config._ConfigFile.getFeature(self,sec,name)
+
 #----------------------------------------------------------------------
 # Server Directories
 
@@ -501,15 +511,15 @@
     _syntax = {
         'Directory': { "__SECTION__": ("REQUIRE", None, None),
                        "Version": ("REQUIRE", None, None),
-                       "Published": ("REQUIRE", C._parseTime, None),
-                       "Valid-After": ("REQUIRE", C._parseDate, None),
-                       "Valid-Until": ("REQUIRE", C._parseDate, None),
+                       "Published": ("REQUIRE", "time", None),
+                       "Valid-After": ("REQUIRE", "date", None),
+                       "Valid-Until": ("REQUIRE", "date", None),
                        "Recommended-Servers": ("REQUIRE", None, None),
                        },
         'Signature': {"__SECTION__": ("REQUIRE", None, None),
-                 "DirectoryIdentity": ("REQUIRE", C._parsePublicKey, None),
-                 "DirectoryDigest": ("REQUIRE", C._parseBase64, None),
-                 "DirectorySignature": ("REQUIRE", C._parseBase64, None),
+                 "DirectoryIdentity": ("REQUIRE", "publicKey", None),
+                 "DirectoryDigest": ("REQUIRE", "base64", None),
+                 "DirectorySignature": ("REQUIRE", "base64", None),
                       },
         'Recommended-Software': {"__SECTION__": ("ALLOW", None, None),
                 "MixminionClient": ("ALLOW", None, None),

Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.159
retrieving revision 1.160
diff -u -d -r1.159 -r1.160
--- test.py	7 Nov 2003 05:44:40 -0000	1.159
+++ test.py	7 Nov 2003 07:03:28 -0000	1.160
@@ -3888,14 +3888,14 @@
                 'Sec2' : {'Fob': ('ALLOW*', None, None),
                           'Bap': ('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),
+                'Sec3' : {'IntAS': ('ALLOW', "int", None),
+                          'IntAS2': ('ALLOW', "int", None),
+                          'IntASD': ('ALLOW', "int", "5"),
+                          'IntASD2': ('ALLOW', "int", "5"),
+                          'IntAM': ('ALLOW*', "int", None),
+                          'IntAMD': ('ALLOW*', "int", ["5", "2"]),
+                          'IntAMD2': ('ALLOW*', "int", ["5", "2"]),
+                          'IntRS': ('REQUIRE', "int", None),
                           'Quz' : ('ALLOW', None, None) }
                 }
     def __init__(self, fname=None, string=None, restrict=0):
@@ -4878,8 +4878,7 @@
     def getName(self):
         return "TestModule"
     def getConfigSyntax(self):
-        return { "Example" : { "Foo" : ("REQUIRE",
-                                        mixminion.Config._parseInt, None) } }
+        return { "Example" : { "Foo" : ("REQUIRE", "int", None) } }
     def validateConfig(self, cfg, lines, contents):
         if cfg['Example'] is not None:
             if cfg['Example'].get('Foo',1) % 2 == 0:

Index: testSupport.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/testSupport.py,v
retrieving revision 1.21
retrieving revision 1.22
diff -u -d -r1.21 -r1.22
--- testSupport.py	2 Oct 2003 21:46:23 -0000	1.21
+++ testSupport.py	7 Nov 2003 07:03:28 -0000	1.22
@@ -46,8 +46,8 @@
     def getConfigSyntax(self):
         return { 'Testing/DirectoryDump':
                  { 'Location' : ('REQUIRE', None, None),
-                   'UseQueue': ('REQUIRE', _parseBoolean, None),
-                   'Retry' : ('ALLOW', _parseIntervalList,
+                   'UseQueue': ('REQUIRE', "boolean", None),
+                   'Retry' : ('ALLOW', "intervalList",
                               "every 1 min for 10 min") } }
 
     def validateConfig(self, config, lines, contents):