[CalendarServer-changes] [10982] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Apr 2 18:26:13 PDT 2013


Revision: 10982
          http://trac.calendarserver.org//changeset/10982
Author:   sagen at apple.com
Date:     2013-04-02 18:26:13 -0700 (Tue, 02 Apr 2013)
Log Message:
-----------
Group Cacher sidecar is gone, and calendarserver_manage_principals has access to the store in order to schedule group cacher updates.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/calendarserver/tap/util.py
    CalendarServer/trunk/calendarserver/tools/cmdline.py
    CalendarServer/trunk/calendarserver/tools/gateway.py
    CalendarServer/trunk/calendarserver/tools/principals.py
    CalendarServer/trunk/calendarserver/tools/purge.py
    CalendarServer/trunk/calendarserver/tools/test/principals/caldavd.plist
    CalendarServer/trunk/calendarserver/tools/test/test_principals.py
    CalendarServer/trunk/calendarserver/tools/util.py
    CalendarServer/trunk/calendarserver/webadmin/resource.py
    CalendarServer/trunk/conf/auth/augments-test.xml
    CalendarServer/trunk/conf/auth/resources-test.xml
    CalendarServer/trunk/twext/enterprise/queue.py
    CalendarServer/trunk/twext/enterprise/test/test_queue.py
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/trunk/twistedcaldav/directory/directory.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_inbound.py
    CalendarServer/trunk/twistedcaldav/test/util.py

Property Changed:
----------------
    CalendarServer/trunk/conf/auth/resources-test.xml

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -78,6 +78,7 @@
 from twistedcaldav.upgrade import UpgradeFileSystemFormatService, PostDBImportService
 
 from calendarserver.tap.util import pgServiceFromConfig, getDBPool, MemoryLimitService
+from calendarserver.tap.util import directoryFromConfig
 
 from twext.enterprise.ienterprise import POSTGRES_DIALECT
 from twext.enterprise.ienterprise import ORACLE_DIALECT
@@ -1054,9 +1055,10 @@
                 if observers:
                     pushDistributor = PushDistributor(observers)
 
+            directory = result.rootResource.getDirectory()
+            
             # Optionally set up mail retrieval
             if config.Scheduling.iMIP.Enabled:
-                directory = result.rootResource.getDirectory()
                 mailRetriever = MailRetriever(store, directory,
                     config.Scheduling.iMIP.Receiving)
                 mailRetriever.setServiceParent(result)
@@ -1445,7 +1447,41 @@
             spawner.setServiceParent(multi)
             if config.UseMetaFD:
                 cl.setServiceParent(multi)
+
+            directory = directoryFromConfig(config)
+            rootResource = getRootResource(config, store, [])
+
+            # Optionally set up mail retrieval
+            if config.Scheduling.iMIP.Enabled:
+                mailRetriever = MailRetriever(store, directory,
+                    config.Scheduling.iMIP.Receiving)
+                mailRetriever.setServiceParent(multi)
+            else:
+                mailRetriever = None
+
+            # Optionally set up group cacher
+            if config.GroupCaching.Enabled:
+                groupCacher = GroupMembershipCacheUpdater(
+                    calendaruserproxy.ProxyDBService,
+                    directory,
+                    config.GroupCaching.UpdateSeconds,
+                    config.GroupCaching.ExpireSeconds,
+                    namespace=config.GroupCaching.MemcachedPool,
+                    useExternalProxies=config.GroupCaching.UseExternalProxies
+                    )
+            else:
+                groupCacher = None
+
+            def decorateTransaction(txn):
+                txn._pushDistributor = None
+                txn._rootResource = rootResource
+                txn._mailRetriever = mailRetriever
+                txn._groupCacher = groupCacher
+
+            store.callWithNewTransactions(decorateTransaction)
+
             return multi
+
         ssvc = self.storageService(spawnerSvcCreator, uid, gid)
         ssvc.setServiceParent(s)
         return s

Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/tap/util.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -650,6 +650,7 @@
             config.WebCalendarRoot,
             root,
             directory,
+            newStore,
             principalCollections=(principalCollection,),
         )
         root.putChild("admin", webAdmin)

Modified: CalendarServer/trunk/calendarserver/tools/cmdline.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/cmdline.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/tools/cmdline.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -24,9 +24,14 @@
 from twext.python.log import StandardIOObserver
 
 from twistedcaldav.config import ConfigurationError
+from twisted.internet.defer import inlineCallbacks
 
 import os
 import sys
+from calendarserver.tap.util import getRootResource
+from twisted.application.service import Service
+from errno import ENOENT, EACCES
+from twext.enterprise.queue import NonPerformingQueuer
 
 # TODO: direct unit tests for these functions.
 
@@ -85,6 +90,10 @@
         autoDisableMemcached(config)
 
         maker = serviceMaker()
+
+        # Only perform post-import duties if someone has explicitly said to
+        maker.doPostImport =  getattr(maker, "doPostImport", False)
+
         options = CalDAVOptions
         service = maker.makeService(options)
 
@@ -98,3 +107,49 @@
         return
 
     reactor.run()
+
+
+
+class WorkerService(Service):
+
+    def __init__(self, store):
+        self._store = store
+        # Work can be queued but will not be performed by the command line tool
+        store.queuer = NonPerformingQueuer()
+
+
+    def rootResource(self):
+        try:
+            from twistedcaldav.config import config
+            rootResource = getRootResource(config, self._store)
+        except OSError, e:
+            if e.errno == ENOENT:
+                # Trying to re-write resources.xml but its parent directory does
+                # not exist.  The server's never been started, so we're missing
+                # state required to do any work.
+                raise ConfigurationError(
+                    "It appears that the server has never been started.\n"
+                    "Please start it at least once before running this tool.")
+            elif e.errno == EACCES:
+                # Trying to re-write resources.xml but it is not writable by the
+                # current user.  This most likely means we're in a system
+                # configuration and the user doesn't have sufficient privileges
+                # to do the other things the tool might need to do either.
+                raise ConfigurationError("You must run this tool as root.")
+            else:
+                raise
+        return rootResource
+
+    @inlineCallbacks
+    def startService(self):
+        from twisted.internet import reactor
+        try:
+            yield self.doWork()
+        except ConfigurationError, ce:
+            sys.stderr.write("Error: %s\n" % (str(ce),))
+        except Exception, e:
+            sys.stderr.write("Error: %s\n" % (e,))
+            raise
+        finally:
+            reactor.stop()
+

Modified: CalendarServer/trunk/calendarserver/tools/gateway.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/gateway.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/tools/gateway.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -28,10 +28,11 @@
 from twistedcaldav.directory.directory import DirectoryError
 from txdav.xml import element as davxml
 
-from calendarserver.tools.principals import (
+from calendarserver.tools.util import (
     principalForPrincipalID, proxySubprincipal, addProxy, removeProxy,
-    getProxies, setProxies, ProxyError, ProxyWarning, updateRecord
+    ProxyError, ProxyWarning
 )
+from calendarserver.tools.principals import getProxies, setProxies, updateRecord
 from calendarserver.tools.purge import WorkerService, PurgeOldEventsService, DEFAULT_BATCH_SIZE, DEFAULT_RETAIN_DAYS
 from calendarserver.tools.cmdline import utilityMain
 
@@ -212,7 +213,7 @@
         readProxies = command.get("ReadProxies", None)
         writeProxies = command.get("WriteProxies", None)
         principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(principal, readProxies, writeProxies, directory=self.dir))
+        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
 
         respondWithRecordsOfType(self.dir, command, "locations")
 
@@ -260,7 +261,7 @@
         readProxies = command.get("ReadProxies", None)
         writeProxies = command.get("WriteProxies", None)
         principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(principal, readProxies, writeProxies, directory=self.dir))
+        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
 
         yield self.command_getLocationAttributes(command)
 
@@ -300,7 +301,7 @@
         readProxies = command.get("ReadProxies", None)
         writeProxies = command.get("WriteProxies", None)
         principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(principal, readProxies, writeProxies, directory=self.dir))
+        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
 
         respondWithRecordsOfType(self.dir, command, "resources")
 
@@ -328,7 +329,7 @@
         readProxies = command.get("ReadProxies", None)
         writeProxies = command.get("WriteProxies", None)
         principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(principal, readProxies, writeProxies, directory=self.dir))
+        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
 
         yield self.command_getResourceAttributes(command)
 
@@ -370,7 +371,7 @@
             respondWithError("Proxy not found: %s" % (command['Proxy'],))
             return
         try:
-            (yield addProxy(principal, "write", proxy))
+            (yield addProxy(self.root, self.dir, self.store, principal, "write", proxy))
         except ProxyError, e:
             respondWithError(str(e))
             return
@@ -390,7 +391,7 @@
             respondWithError("Proxy not found: %s" % (command['Proxy'],))
             return
         try:
-            (yield removeProxy(principal, proxy, proxyTypes=("write",)))
+            (yield removeProxy(self.root, self.dir, self.store, principal, proxy, proxyTypes=("write",)))
         except ProxyError, e:
             respondWithError(str(e))
             return
@@ -419,7 +420,7 @@
             respondWithError("Proxy not found: %s" % (command['Proxy'],))
             return
         try:
-            (yield addProxy(principal, "read", proxy))
+            (yield addProxy(self.root, self.dir, self.store, principal, "read", proxy))
         except ProxyError, e:
             respondWithError(str(e))
             return
@@ -439,7 +440,7 @@
             respondWithError("Proxy not found: %s" % (command['Proxy'],))
             return
         try:
-            (yield removeProxy(principal, proxy, proxyTypes=("read",)))
+            (yield removeProxy(self.root, self.dir, self.store, principal, proxy, proxyTypes=("read",)))
         except ProxyError, e:
             respondWithError(str(e))
             return

Modified: CalendarServer/trunk/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/principals.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/tools/principals.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -20,31 +20,33 @@
 import sys
 import os
 import operator
-import signal
 from getopt import getopt, GetoptError
 from uuid import UUID
 
 from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from txdav.xml import element as davxml
 
 from txdav.xml.base import decodeXMLName, encodeXMLName
 
 from twistedcaldav.config import config
 from twistedcaldav.directory.directory import UnknownRecordTypeError, DirectoryError
+from twistedcaldav.directory.directory import scheduleNextGroupCachingUpdate
 
-from calendarserver.tools.util import booleanArgument 
+from calendarserver.tools.util import (
+    booleanArgument, proxySubprincipal, action_addProxyPrincipal,
+    principalForPrincipalID, prettyPrincipal, ProxyError,
+    action_removeProxyPrincipal
+)
 from twistedcaldav.directory.augment import allowedAutoScheduleModes
 from calendarserver.tools.cmdline import utilityMain
 
 # FIXME: Move WorkerService to a util module?
 from calendarserver.tools.purge import WorkerService
 
-__all__ = [
-    "principalForPrincipalID", "proxySubprincipal", "addProxy", "removeProxy",
-    "ProxyError", "ProxyWarning", "updateRecord"
-]
+from calendarserver.tools.cmdline import utilityMain, WorkerService
 
+
 def usage(e=None):
     if e:
         if isinstance(e, UnknownRecordTypeError):
@@ -100,17 +102,22 @@
 
 class PrincipalService(WorkerService):
     """
+    Executes principals-related functions in a context which has access to the store
     """
 
+    function = None
     params = []
 
     @inlineCallbacks
     def doWork(self):
         """
+        Calls the function that's been assigned to "function" and passes the root
+        resource, directory, store, and whatever has been assigned to "params".
         """
-        rootResource = self.rootResource()
-        directory = rootResource.getDirectory()
-        yield self.command(rootResource, directory, self._store, *self.params) 
+        if self.function is not None:
+            rootResource = self.rootResource()
+            directory = rootResource.getDirectory()
+            yield self.function(rootResource, directory, self._store, *self.params) 
 
 
 def main():
@@ -265,10 +272,9 @@
         if args:
             usage("Too many arguments")
 
-        for recordType in config.directory.recordTypes():
-            print(recordType)
+        function = runListPrincipalTypes
+        params = ()
 
-        return
 
     elif addType:
 
@@ -289,7 +295,8 @@
         else:
             shortNames = ()
 
-        params = (runAddPrincipal, addType, guid, shortNames, fullName)
+        function = runAddPrincipal
+        params = (addType, guid, shortNames, fullName)
 
 
     elif listPrincipals:
@@ -303,19 +310,13 @@
         if args:
             usage("Too many arguments")
 
-        try:
-            records = list(config.directory.listRecords(listPrincipals))
-            if records:
-                printRecordList(records)
-            else:
-                print("No records of type %s" % (listPrincipals,))
-        except UnknownRecordTypeError, e:
-            usage(e)
+        function = runListPrincipals
+        params = (listPrincipals,)
 
-        return
 
     elif searchPrincipals:
-        params = (runSearch, searchPrincipals)
+        function = runSearch
+        params = (searchPrincipals,)
 
     else:
         #
@@ -331,157 +332,96 @@
             except ValueError, e:
                 abort(e)
 
-        params = (runPrincipalActions, args, principalActions)
+        function = runPrincipalActions
+        params = (args, principalActions)
 
 
+    PrincipalService.function = function
     PrincipalService.params = params
     utilityMain(configFileName, PrincipalService, verbose=verbose)
 
 
- at inlineCallbacks
-def runPrincipalActions(rootResource, directory, store, principalIDs, actions):
-    try:
-        for principalID in principalIDs:
-            # Resolve the given principal IDs to principals
-            try:
-                principal = principalForPrincipalID(principalID)
-            except ValueError:
-                principal = None
+def runListPrincipalTypes(service, rootResource, directory, store):
+    for recordType in directory.recordTypes():
+        print(recordType)
+    return succeed(None)
 
-            if principal is None:
-                sys.stderr.write("Invalid principal ID: %s\n" % (principalID,))
-                continue
 
-            # Performs requested actions
-            for action in actions:
-                (yield action[0](rootResource, directory, store, principal,
-                    *action[1:]))
-                print("")
-
-    finally:
-        #
-        # Stop the reactor
-        #
-        reactor.stop()
-
- at inlineCallbacks
-def runSearch(rootResource, directory, store, searchTerm):
-
+def runListPrincipals(service, rootResource, directory, store, listPrincipals):
     try:
-        fields = []
-        for fieldName in ("fullName", "firstName", "lastName", "emailAddresses"):
-            fields.append((fieldName, searchTerm, True, "contains"))
-
-        records = list((yield directory.recordsMatchingTokens(searchTerm.strip().split())))
+        records = list(directory.listRecords(listPrincipals))
         if records:
-            records.sort(key=operator.attrgetter('fullName'))
-            print("%d matches found:" % (len(records),))
-            for record in records:
-                print("\n%s (%s)" % (record.fullName,
-                    { "users"     : "User",
-                      "groups"    : "Group",
-                      "locations" : "Place",
-                      "resources" : "Resource",
-                    }.get(record.recordType),
-                ))
-                print("   GUID: %s" % (record.guid,))
-                print("   Record name(s): %s" % (", ".join(record.shortNames),))
-                if record.authIDs:
-                    print("   Auth ID(s): %s" % (", ".join(record.authIDs),))
-                if record.emailAddresses:
-                    print("   Email(s): %s" % (", ".join(record.emailAddresses),))
+            printRecordList(records)
         else:
-            print("No matches found")
+            print("No records of type %s" % (listPrincipals,))
+    except UnknownRecordTypeError, e:
+        usage(e)
+    return succeed(None)
 
-        print("")
 
-    finally:
-        #
-        # Stop the reactor
-        #
-        reactor.stop()
-
 @inlineCallbacks
-def runAddPrincipal(addType, guid, shortNames, fullName):
-    try:
+def runPrincipalActions(service, rootResource, directory, store, principalIDs,
+    actions):
+    for principalID in principalIDs:
+        # Resolve the given principal IDs to principals
         try:
-            yield updateRecord(True, config.directory, addType, guid=guid,
-                shortNames=shortNames, fullName=fullName)
-            print("Added '%s'" % (fullName,))
-        except DirectoryError, e:
-            print(e)
+            principal = principalForPrincipalID(principalID, directory=directory)
+        except ValueError:
+            principal = None
 
-    finally:
-        #
-        # Stop the reactor
-        #
-        reactor.stop()
+        if principal is None:
+            sys.stderr.write("Invalid principal ID: %s\n" % (principalID,))
+            continue
 
+        # Performs requested actions
+        for action in actions:
+            (yield action[0](rootResource, directory, store, principal,
+                *action[1:]))
+            print("")
 
-def principalForPrincipalID(principalID, checkOnly=False, directory=None):
-    
-    # Allow a directory parameter to be passed in, but default to config.directory
-    # But config.directory isn't set right away, so only use it when we're doing more 
-    # than checking.
-    if not checkOnly and not directory:
-        directory = config.directory
 
-    if principalID.startswith("/"):
-        segments = principalID.strip("/").split("/")
-        if (len(segments) == 3 and
-            segments[0] == "principals" and segments[1] == "__uids__"):
-            uid = segments[2]
-        else:
-            raise ValueError("Can't resolve all paths yet")
+ at inlineCallbacks
+def runSearch(service, rootResource, directory, store, searchTerm):
 
-        if checkOnly:
-            return None
+    fields = []
+    for fieldName in ("fullName", "firstName", "lastName", "emailAddresses"):
+        fields.append((fieldName, searchTerm, True, "contains"))
 
-        return directory.principalCollection.principalForUID(uid)
+    records = list((yield directory.recordsMatchingTokens(searchTerm.strip().split())))
+    if records:
+        records.sort(key=operator.attrgetter('fullName'))
+        print("%d matches found:" % (len(records),))
+        for record in records:
+            print("\n%s (%s)" % (record.fullName,
+                { "users"     : "User",
+                  "groups"    : "Group",
+                  "locations" : "Place",
+                  "resources" : "Resource",
+                }.get(record.recordType),
+            ))
+            print("   GUID: %s" % (record.guid,))
+            print("   Record name(s): %s" % (", ".join(record.shortNames),))
+            if record.authIDs:
+                print("   Auth ID(s): %s" % (", ".join(record.authIDs),))
+            if record.emailAddresses:
+                print("   Email(s): %s" % (", ".join(record.emailAddresses),))
+    else:
+        print("No matches found")
 
+    print("")
 
-    if principalID.startswith("("):
-        try:
-            i = principalID.index(")")
 
-            if checkOnly:
-                return None
-
-            recordType = principalID[1:i]
-            shortName = principalID[i+1:]
-
-            if not recordType or not shortName or "(" in recordType:
-                raise ValueError()
-
-            return directory.principalCollection.principalForShortName(recordType, shortName)
-
-        except ValueError:
-            pass
-
-    if ":" in principalID:
-        if checkOnly:
-            return None
-
-        recordType, shortName = principalID.split(":", 1)
-
-        return directory.principalCollection.principalForShortName(recordType, shortName)
-
+ at inlineCallbacks
+def runAddPrincipal(service, rootResource, directory, store, addType, guid,
+    shortNames, fullName):
     try:
-        UUID(principalID)
+        yield updateRecord(True, directory, addType, guid=guid,
+            shortNames=shortNames, fullName=fullName)
+        print("Added '%s'" % (fullName,))
+    except DirectoryError, e:
+        print(e)
 
-        if checkOnly:
-            return None
 
-        x = directory.principalCollection.principalForUID(principalID)
-        return x
-    except ValueError:
-        pass
-
-    raise ValueError("Invalid principal identifier: %s" % (principalID,))
-
-def proxySubprincipal(principal, proxyType):
-    return principal.getChild("calendar-proxy-" + proxyType)
-
 def action_removePrincipal(rootResource, directory, store, principal):
     record = principal.record
     fullName = record.fullName
@@ -530,57 +470,17 @@
 @inlineCallbacks
 def action_addProxy(rootResource, directory, store, principal, proxyType, *proxyIDs):
     for proxyID in proxyIDs:
-        proxyPrincipal = principalForPrincipalID(proxyID)
+        proxyPrincipal = principalForPrincipalID(proxyID, directory=directory)
         if proxyPrincipal is None:
             print("Invalid principal ID: %s" % (proxyID,))
         else:
-            (yield action_addProxyPrincipal(principal, proxyType, proxyPrincipal))
+            (yield action_addProxyPrincipal(rootResource, directory, store, 
+                principal, proxyType, proxyPrincipal))
 
- at inlineCallbacks
-def action_addProxyPrincipal(rootResource, directory, store, principal, proxyType, proxyPrincipal):
-    try:
-        (yield addProxy(principal, proxyType, proxyPrincipal))
-        print("Added %s as a %s proxy for %s" % (
-            prettyPrincipal(proxyPrincipal), proxyType,
-            prettyPrincipal(principal)))
-    except ProxyError, e:
-        print("Error:", e)
-    except ProxyWarning, e:
-        print(e)
 
- at inlineCallbacks
-def addProxy(principal, proxyType, proxyPrincipal):
-    proxyURL = proxyPrincipal.url()
 
-    subPrincipal = proxySubprincipal(principal, proxyType)
-    if subPrincipal is None:
-        raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
-            prettyPrincipal(principal)))
-
-    membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-
-    for memberURL in membersProperty.children:
-        if str(memberURL) == proxyURL:
-            raise ProxyWarning("%s is already a %s proxy for %s" % (
-                prettyPrincipal(proxyPrincipal), proxyType,
-                prettyPrincipal(principal)))
-
-    else:
-        memberURLs = list(membersProperty.children)
-        memberURLs.append(davxml.HRef(proxyURL))
-        membersProperty = davxml.GroupMemberSet(*memberURLs)
-        (yield subPrincipal.writeProperty(membersProperty, None))
-
-    proxyTypes = ["read", "write"]
-    proxyTypes.remove(proxyType)
-
-    (yield action_removeProxyPrincipal(principal, proxyPrincipal, proxyTypes=proxyTypes))
-
-    triggerGroupCacherUpdate(config)
-
-
 @inlineCallbacks
-def setProxies(principal, readProxyPrincipals, writeProxyPrincipals, directory=None):
+def setProxies(store, principal, readProxyPrincipals, writeProxyPrincipals, directory=None):
     """
     Set read/write proxies en masse for a principal
     @param principal: DirectoryPrincipalResource
@@ -605,8 +505,9 @@
             proxyURL = proxyPrincipal.url()
             memberURLs.append(davxml.HRef(proxyURL))
         membersProperty = davxml.GroupMemberSet(*memberURLs)
-        (yield subPrincipal.writeProperty(membersProperty, None))
-        triggerGroupCacherUpdate(config)
+        yield subPrincipal.writeProperty(membersProperty, None)
+        if store is not None:
+            yield scheduleNextGroupCachingUpdate(store, 0)
 
 
 @inlineCallbacks
@@ -635,59 +536,16 @@
 @inlineCallbacks
 def action_removeProxy(rootResource, directory, store, principal, *proxyIDs, **kwargs):
     for proxyID in proxyIDs:
-        proxyPrincipal = principalForPrincipalID(proxyID)
+        proxyPrincipal = principalForPrincipalID(proxyID, directory=directory)
         if proxyPrincipal is None:
             print("Invalid principal ID: %s" % (proxyID,))
         else:
-            (yield action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs))
+            (yield action_removeProxyPrincipal(rootResource, directory, store,
+                principal, proxyPrincipal, **kwargs))
 
- at inlineCallbacks
-def action_removeProxyPrincipal(rootResource, directory, store, principal, proxyPrincipal, **kwargs):
-    try:
-        removed = (yield removeProxy(principal, proxyPrincipal, **kwargs))
-        if removed:
-            print("Removed %s as a proxy for %s" % (
-                prettyPrincipal(proxyPrincipal),
-                prettyPrincipal(principal)))
-    except ProxyError, e:
-        print("Error:", e)
-    except ProxyWarning, e:
-        print(e)
 
 
- at inlineCallbacks
-def removeProxy(principal, proxyPrincipal, **kwargs):
-    removed = False
-    proxyTypes = kwargs.get("proxyTypes", ("read", "write"))
-    for proxyType in proxyTypes:
-        proxyURL = proxyPrincipal.url()
 
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is None:
-            raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
-                prettyPrincipal(principal)))
-
-        membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-
-        memberURLs = [
-            m for m in membersProperty.children
-            if str(m) != proxyURL
-        ]
-
-        if len(memberURLs) == len(membersProperty.children):
-            # No change
-            continue
-        else:
-            removed = True
-
-        membersProperty = davxml.GroupMemberSet(*memberURLs)
-        (yield subPrincipal.writeProperty(membersProperty, None))
-
-    if removed:
-        triggerGroupCacherUpdate(config)
-    returnValue(removed)
-
-
 @inlineCallbacks
 def action_setAutoSchedule(rootResource, directory, store, principal, autoSchedule):
     if principal.record.recordType == "groups":
@@ -759,7 +617,7 @@
         print("Setting auto-accept-group for %s is not allowed." % (principal,))
 
     else:
-        groupPrincipal = principalForPrincipalID(autoAcceptGroup)
+        groupPrincipal = principalForPrincipalID(autoAcceptGroup, directory=directory)
         if groupPrincipal is None or groupPrincipal.record.recordType != "groups":
             print("Invalid principal ID: %s" % (autoAcceptGroup,))
         else:
@@ -802,17 +660,7 @@
         pass
     sys.exit(status)
 
-class ProxyError(Exception):
-    """
-    Raised when proxy assignments cannot be performed
-    """
 
-class ProxyWarning(Exception):
-    """
-    Raised for harmless proxy assignment failures such as trying to add a
-    duplicate or remove a non-existent assignment.
-    """
-
 def parseCreationArgs(args):
     """
     Look at the command line arguments for --add, and figure out which
@@ -865,10 +713,6 @@
     for fullName, shortName, guid in results:
         print(format % (fullName, shortName, guid))
 
-def prettyPrincipal(principal):
-    record = principal.record
-    return "\"%s\" (%s:%s)" % (record.fullName, record.recordType,
-        record.shortNames[0])
 
 
 @inlineCallbacks
@@ -949,28 +793,6 @@
     returnValue(record)
 
 
-def triggerGroupCacherUpdate(config, killMethod=None):
-    """
-    Look up the pid of the group cacher sidecar and HUP it to trigger an update
-    """
-    if killMethod is None:
-        killMethod = os.kill
 
-    pidFilename = os.path.join(config.RunRoot, "groupcacher.pid")
-    if os.path.exists(pidFilename):
-        pidFile = open(pidFilename, "r")
-        pid = pidFile.read().strip()
-        pidFile.close()
-        try:
-            pid = int(pid)
-        except ValueError:
-            return
-        try:
-            killMethod(pid, signal.SIGHUP)
-        except OSError:
-            pass
-
-
-
 if __name__ == "__main__":
     main()

Modified: CalendarServer/trunk/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/purge.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/tools/purge.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -18,12 +18,10 @@
 from __future__ import print_function
 
 from calendarserver.tap.util import FakeRequest
-from calendarserver.tap.util import getRootResource
 from calendarserver.tools import tables
-from calendarserver.tools.cmdline import utilityMain
-from calendarserver.tools.principals import removeProxy
+from calendarserver.tools.cmdline import utilityMain, WorkerService
+from calendarserver.tools.util import removeProxy
 
-from errno import ENOENT, EACCES
 from getopt import getopt, GetoptError
 
 from pycalendar.datetime import PyCalendarDateTime
@@ -31,13 +29,10 @@
 from twext.python.log import Logger
 from twext.web2.responsecode import NO_CONTENT
 
-from twisted.application.service import Service
-from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from twistedcaldav import caldavxml
 from twistedcaldav.caldavxml import TimeRange
-from twistedcaldav.config import config, ConfigurationError
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 from twistedcaldav.directory.directory import DirectoryRecord
 from twistedcaldav.method.put_common import StoreCalendarObjectResource
@@ -45,6 +40,7 @@
 
 from txdav.xml import element as davxml
 
+
 import collections
 import os
 import sys
@@ -54,49 +50,9 @@
 DEFAULT_BATCH_SIZE = 100
 DEFAULT_RETAIN_DAYS = 365
 
-class WorkerService(Service):
 
-    def __init__(self, store):
-        self._store = store
 
 
-    def rootResource(self):
-        try:
-            rootResource = getRootResource(config, self._store)
-        except OSError, e:
-            if e.errno == ENOENT:
-                # Trying to re-write resources.xml but its parent directory does
-                # not exist.  The server's never been started, so we're missing
-                # state required to do any work.  (Plus, what would be the point
-                # of purging stuff from a server that's completely empty?)
-                raise ConfigurationError(
-                    "It appears that the server has never been started.\n"
-                    "Please start it at least once before purging anything.")
-            elif e.errno == EACCES:
-                # Trying to re-write resources.xml but it is not writable by the
-                # current user.  This most likely means we're in a system
-                # configuration and the user doesn't have sufficient privileges
-                # to do the other things the tool might need to do either.
-                raise ConfigurationError("You must run this tool as root.")
-            else:
-                raise
-        return rootResource
-
-
-    @inlineCallbacks
-    def startService(self):
-        try:
-            yield self.doWork()
-        except ConfigurationError, ce:
-            sys.stderr.write("Error: %s\n" % (str(ce),))
-        except Exception, e:
-            sys.stderr.write("Error: %s\n" % (e,))
-            raise
-        finally:
-            reactor.stop()
-
-
-
 class PurgeOldEventsService(WorkerService):
 
     cutoff = None
@@ -1163,9 +1119,8 @@
             return cls.CANCELEVENT_NOT_MODIFIED
 
 
-    @classmethod
     @inlineCallbacks
-    def _purgeProxyAssignments(cls, principal):
+    def _purgeProxyAssignments(self, principal):
 
         assignments = []
 
@@ -1174,7 +1129,7 @@
             proxyFor = (yield principal.proxyFor(proxyType == "write"))
             for other in proxyFor:
                 assignments.append((principal.record.uid, proxyType, other.record.uid))
-                (yield removeProxy(other, principal))
+                (yield removeProxy(self.root, self.directory, self._store, other, principal))
 
             subPrincipal = principal.getChild("calendar-proxy-" + proxyType)
             proxies = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))

Modified: CalendarServer/trunk/calendarserver/tools/test/principals/caldavd.plist
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/principals/caldavd.plist	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/tools/test/principals/caldavd.plist	2013-04-03 01:26:13 UTC (rev 10982)
@@ -79,23 +79,23 @@
 
     <!-- Data root -->
     <key>DataRoot</key>
-    <string>Data</string>
+    <string>%(DataRoot)s</string>
 
     <!-- Document root -->
     <key>DocumentRoot</key>
-    <string>Documents</string>
+    <string>%(DocumentRoot)s</string>
 
     <!-- Configuration root -->
     <key>ConfigRoot</key>
-    <string>/etc/caldavd</string>
+    <string>Config</string>
 
     <!-- Log root -->
     <key>LogRoot</key>
-    <string>/var/log/caldavd</string>
+    <string>%(LogRoot)s</string>
 
     <!-- Run root -->
     <key>RunRoot</key>
-    <string>/var/run</string>
+    <string>%(LogRoot)s</string>
 
     <!-- Child aliases -->
     <key>Aliases</key>
@@ -279,7 +279,7 @@
      -->
 
 	<key>ProxyLoadFromFile</key>
-    <string>conf/auth/proxies-test.xml</string>
+    <string></string>
 
     <!--
         Special principals

Modified: CalendarServer/trunk/calendarserver/tools/test/test_principals.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_principals.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/tools/test/test_principals.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -15,7 +15,6 @@
 ##
 
 import os
-import signal
 import sys
 
 from twext.python.filepath import CachingFilePath as FilePath
@@ -31,8 +30,7 @@
 
 from calendarserver.tap.util import directoryFromConfig
 from calendarserver.tools.principals import (parseCreationArgs, matchStrings,
-    updateRecord, principalForPrincipalID, getProxies, setProxies,
-    triggerGroupCacherUpdate)
+    updateRecord, principalForPrincipalID, getProxies, setProxies)
 
 
 class ManagePrincipalsTestCase(TestCase):
@@ -53,6 +51,9 @@
 
         newConfig = template % {
             "ServerRoot" : os.path.abspath(config.ServerRoot),
+            "DataRoot" : os.path.abspath(config.DataRoot),
+            "DocumentRoot" : os.path.abspath(config.DocumentRoot),
+            "LogRoot" : os.path.abspath(config.LogRoot),
         }
         configFilePath = FilePath(os.path.join(config.ConfigRoot, "caldavd.plist"))
         configFilePath.setContent(newConfig)
@@ -339,36 +340,13 @@
         self.assertEquals(readProxies, []) # initially empty
         self.assertEquals(writeProxies, []) # initially empty
 
-        (yield setProxies(principal, ["users:user03", "users:user04"], ["users:user05"], directory=directory))
+        (yield setProxies(None, principal, ["users:user03", "users:user04"], ["users:user05"], directory=directory))
         readProxies, writeProxies = (yield getProxies(principal, directory=directory))
         self.assertEquals(set(readProxies), set(["user03", "user04"]))
         self.assertEquals(set(writeProxies), set(["user05"]))
 
         # Using None for a proxy list indicates a no-op
-        (yield setProxies(principal, [], None, directory=directory))
+        (yield setProxies(None, principal, [], None, directory=directory))
         readProxies, writeProxies = (yield getProxies(principal, directory=directory))
         self.assertEquals(readProxies, []) # now empty
         self.assertEquals(set(writeProxies), set(["user05"])) # unchanged
-
-
-    def test_triggerGroupCacherUpdate(self):
-        """
-        Verify triggerGroupCacherUpdate can read a pidfile and send a SIGHUP
-        """
-
-        self.calledArgs = None
-        def killMethod(pid, sig):
-            self.calledArgs = (pid, sig)
-
-        class StubConfig(object):
-            def __init__(self, runRootPath):
-                self.RunRoot = runRootPath
-
-        runRootDir = FilePath(self.mktemp())
-        runRootDir.createDirectory()
-        pidFile = runRootDir.child("groupcacher.pid")
-        pidFile.setContent("1234")
-        testConfig = StubConfig(runRootDir.path)
-        triggerGroupCacherUpdate(testConfig, killMethod=killMethod)
-        self.assertEquals(self.calledArgs, (1234, signal.SIGHUP))
-        runRootDir.remove()

Modified: CalendarServer/trunk/calendarserver/tools/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/util.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/tools/util.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -31,21 +31,27 @@
 import socket
 from pwd import getpwnam
 from grp import getgrnam
+from uuid import UUID
 
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+
+
 from twisted.python.filepath import FilePath
 from twisted.python.reflect import namedClass
 from twext.python.log import Logger
+from twisted.internet.defer import inlineCallbacks, returnValue
 
+from txdav.xml import element as davxml
 
 from calendarserver.provision.root import RootResource
 
 from twistedcaldav import memcachepool
-from twistedcaldav.config import config, ConfigurationError
 from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.directory.directory import scheduleNextGroupCachingUpdate
 from calendarserver.push.notifier import NotifierFactory
-from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 
 from txdav.common.datastore.file import CommonDataStore
 
@@ -312,3 +318,176 @@
 
 
 
+def principalForPrincipalID(principalID, checkOnly=False, directory=None):
+    
+    # Allow a directory parameter to be passed in, but default to config.directory
+    # But config.directory isn't set right away, so only use it when we're doing more 
+    # than checking.
+    if not checkOnly and not directory:
+        directory = config.directory
+
+    if principalID.startswith("/"):
+        segments = principalID.strip("/").split("/")
+        if (len(segments) == 3 and
+            segments[0] == "principals" and segments[1] == "__uids__"):
+            uid = segments[2]
+        else:
+            raise ValueError("Can't resolve all paths yet")
+
+        if checkOnly:
+            return None
+
+        return directory.principalCollection.principalForUID(uid)
+
+
+    if principalID.startswith("("):
+        try:
+            i = principalID.index(")")
+
+            if checkOnly:
+                return None
+
+            recordType = principalID[1:i]
+            shortName = principalID[i+1:]
+
+            if not recordType or not shortName or "(" in recordType:
+                raise ValueError()
+
+            return directory.principalCollection.principalForShortName(recordType, shortName)
+
+        except ValueError:
+            pass
+
+    if ":" in principalID:
+        if checkOnly:
+            return None
+
+        recordType, shortName = principalID.split(":", 1)
+
+        return directory.principalCollection.principalForShortName(recordType, shortName)
+
+    try:
+        UUID(principalID)
+
+        if checkOnly:
+            return None
+
+        x = directory.principalCollection.principalForUID(principalID)
+        return x
+    except ValueError:
+        pass
+
+    raise ValueError("Invalid principal identifier: %s" % (principalID,))
+
+def proxySubprincipal(principal, proxyType):
+    return principal.getChild("calendar-proxy-" + proxyType)
+
+ at inlineCallbacks
+def action_addProxyPrincipal(rootResource, directory, store, principal, proxyType, proxyPrincipal):
+    try:
+        (yield addProxy(rootResource, directory, store, principal, proxyType, proxyPrincipal))
+        print("Added %s as a %s proxy for %s" % (
+            prettyPrincipal(proxyPrincipal), proxyType,
+            prettyPrincipal(principal)))
+    except ProxyError, e:
+        print("Error:", e)
+    except ProxyWarning, e:
+        print(e)
+
+ at inlineCallbacks
+def action_removeProxyPrincipal(rootResource, directory, store, principal, proxyPrincipal, **kwargs):
+    try:
+        removed = (yield removeProxy(rootResource, directory, store,
+            principal, proxyPrincipal, **kwargs))
+        if removed:
+            print("Removed %s as a proxy for %s" % (
+                prettyPrincipal(proxyPrincipal),
+                prettyPrincipal(principal)))
+    except ProxyError, e:
+        print("Error:", e)
+    except ProxyWarning, e:
+        print(e)
+
+ at inlineCallbacks
+def addProxy(rootResource, directory, store, principal, proxyType, proxyPrincipal):
+    proxyURL = proxyPrincipal.url()
+
+    subPrincipal = proxySubprincipal(principal, proxyType)
+    if subPrincipal is None:
+        raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
+            prettyPrincipal(principal)))
+
+    membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+
+    for memberURL in membersProperty.children:
+        if str(memberURL) == proxyURL:
+            raise ProxyWarning("%s is already a %s proxy for %s" % (
+                prettyPrincipal(proxyPrincipal), proxyType,
+                prettyPrincipal(principal)))
+
+    else:
+        memberURLs = list(membersProperty.children)
+        memberURLs.append(davxml.HRef(proxyURL))
+        membersProperty = davxml.GroupMemberSet(*memberURLs)
+        (yield subPrincipal.writeProperty(membersProperty, None))
+
+    proxyTypes = ["read", "write"]
+    proxyTypes.remove(proxyType)
+
+    (yield action_removeProxyPrincipal(rootResource, directory, store,
+        principal, proxyPrincipal, proxyTypes=proxyTypes))
+
+    yield scheduleNextGroupCachingUpdate(store, 0)
+
+ at inlineCallbacks
+def removeProxy(rootResource, directory, store, principal, proxyPrincipal, **kwargs):
+    removed = False
+    proxyTypes = kwargs.get("proxyTypes", ("read", "write"))
+    for proxyType in proxyTypes:
+        proxyURL = proxyPrincipal.url()
+
+        subPrincipal = proxySubprincipal(principal, proxyType)
+        if subPrincipal is None:
+            raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
+                prettyPrincipal(principal)))
+
+        membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+
+        memberURLs = [
+            m for m in membersProperty.children
+            if str(m) != proxyURL
+        ]
+
+        if len(memberURLs) == len(membersProperty.children):
+            # No change
+            continue
+        else:
+            removed = True
+
+        membersProperty = davxml.GroupMemberSet(*memberURLs)
+        (yield subPrincipal.writeProperty(membersProperty, None))
+
+    if removed:
+        yield scheduleNextGroupCachingUpdate(store, 0)
+    returnValue(removed)
+
+
+
+def prettyPrincipal(principal):
+    record = principal.record
+    return "\"%s\" (%s:%s)" % (record.fullName, record.recordType,
+        record.shortNames[0])
+
+class ProxyError(Exception):
+    """
+    Raised when proxy assignments cannot be performed
+    """
+
+class ProxyWarning(Exception):
+    """
+    Raised for harmless proxy assignment failures such as trying to add a
+    duplicate or remove a non-existent assignment.
+    """
+
+
+

Modified: CalendarServer/trunk/calendarserver/webadmin/resource.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/resource.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/calendarserver/webadmin/resource.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -28,7 +28,7 @@
 import operator
 import urlparse
 
-from calendarserver.tools.principals import (
+from calendarserver.tools.util import (
     principalForPrincipalID, proxySubprincipal, action_addProxyPrincipal,
     action_removeProxyPrincipal
 )
@@ -569,9 +569,10 @@
     Web administration HTTP resource.
     """
 
-    def __init__(self, path, root, directory, principalCollections=()):
+    def __init__(self, path, root, directory, store, principalCollections=()):
         self.root = root
         self.directory = directory
+        self.store = store
         super(WebAdminResource, self).__init__(path,
             principalCollections=principalCollections)
 
@@ -642,16 +643,18 @@
         # Update the proxies if specified.
         for proxyId in removeProxies:
             proxy = self.getResourceById(request, proxyId)
-            (yield action_removeProxyPrincipal(principal, proxy,
-                                               proxyTypes=["read", "write"]))
+            (yield action_removeProxyPrincipal(self.root, self.directory, self.store,
+                principal, proxy, proxyTypes=["read", "write"]))
 
         for proxyId in makeReadProxies:
             proxy = self.getResourceById(request, proxyId)
-            (yield action_addProxyPrincipal(principal, "read", proxy))
+            (yield action_addProxyPrincipal(self.root, self.directory, self.store,
+                principal, "read", proxy))
 
         for proxyId in makeWriteProxies:
             proxy = self.getResourceById(request, proxyId)
-            (yield action_addProxyPrincipal(principal, "write", proxy))
+            (yield action_addProxyPrincipal(self.root, self.directory, self.store,
+                principal, "write", proxy))
 
 
     @inlineCallbacks

Modified: CalendarServer/trunk/conf/auth/augments-test.xml
===================================================================
--- CalendarServer/trunk/conf/auth/augments-test.xml	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/conf/auth/augments-test.xml	2013-04-03 01:26:13 UTC (rev 10982)
@@ -1,21 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2009-2013 Apple Inc. All rights reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
- -->
-
 <!DOCTYPE augments SYSTEM "augments.dtd">
 
 <augments>
@@ -111,4 +94,46 @@
     <enable-addressbook>false</enable-addressbook>
     <auto-schedule>false</auto-schedule>
   </record>
+  <record>
+    <uid>03DFF660-8BCC-4198-8588-DD77F776F518</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>80689D41-DAF8-4189-909C-DB017B271892</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>C38BEE7A-36EE-478C-9DCB-CBF4612AFE65</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+    <auto-accept-group>group01</auto-accept-group>
+  </record>
+  <record>
+    <uid>CCE95217-A57B-481A-AC3D-FEC9AB6CE3A9</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>0CE0BF31-5F9E-4801-A489-8C70CF287F5F</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
 </augments>

Modified: CalendarServer/trunk/conf/auth/resources-test.xml
===================================================================
--- CalendarServer/trunk/conf/auth/resources-test.xml	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/conf/auth/resources-test.xml	2013-04-03 01:26:13 UTC (rev 10982)
@@ -1,94 +1,227 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2006-2013 Apple Inc. All rights reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
- -->
-
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-
 <accounts realm="Test Realm">
-  <location repeat="10">
-    <uid>location%02d</uid>
-    <guid>location%02d</guid>
-    <password>location%02d</password>
-    <name>Room %02d</name>
+  <location>
+    <uid>jupiter</uid>
+    <guid>jupiter</guid>
+    <name>Jupiter Conference Room, Building 2, 1st Floor</name>
   </location>
-  <resource repeat="20">
-    <uid>resource%02d</uid>
-    <guid>resource%02d</guid>
-    <password>resource%02d</password>
-    <name>Resource %02d</name>
-  </resource>
   <location>
+    <uid>uranus</uid>
+    <guid>uranus</guid>
+    <name>Uranus Conference Room, Building 3, 1st Floor</name>
+  </location>
+  <location>
+    <uid>morgensroom</uid>
+    <guid>03DFF660-8BCC-4198-8588-DD77F776F518</guid>
+    <name>Morgen's Room</name>
+  </location>
+  <location>
     <uid>mercury</uid>
     <guid>mercury</guid>
-    <password>test</password>
     <name>Mercury Conference Room, Building 1, 2nd Floor</name>
   </location>
   <location>
-    <uid>venus</uid>
-    <guid>venus</guid>
-    <password>test</password>
-    <name>Venus Conference Room, Building 1, 2nd Floor</name>
+    <uid>location09</uid>
+    <guid>location09</guid>
+    <name>Room 09</name>
   </location>
   <location>
-    <uid>Earth</uid>
-    <guid>Earth</guid>
-    <password>test</password>
-    <name>Earth Conference Room, Building 1, 1st Floor</name>
+    <uid>location08</uid>
+    <guid>location08</guid>
+    <name>Room 08</name>
   </location>
   <location>
+    <uid>location07</uid>
+    <guid>location07</guid>
+    <name>Room 07</name>
+  </location>
+  <location>
+    <uid>location06</uid>
+    <guid>location06</guid>
+    <name>Room 06</name>
+  </location>
+  <location>
+    <uid>location05</uid>
+    <guid>location05</guid>
+    <name>Room 05</name>
+  </location>
+  <location>
+    <uid>location04</uid>
+    <guid>location04</guid>
+    <name>Room 04</name>
+  </location>
+  <location>
+    <uid>location03</uid>
+    <guid>location03</guid>
+    <name>Room 03</name>
+  </location>
+  <location>
+    <uid>location02</uid>
+    <guid>location02</guid>
+    <name>Room 02</name>
+  </location>
+  <location>
+    <uid>location01</uid>
+    <guid>location01</guid>
+    <name>Room 01</name>
+  </location>
+  <location>
+    <uid>delegatedroom</uid>
+    <guid>delegatedroom</guid>
+    <name>Delegated Conference Room</name>
+  </location>
+  <location>
     <uid>mars</uid>
     <guid>redplanet</guid>
-    <password>test</password>
     <name>Mars Conference Room, Building 1, 1st Floor</name>
   </location>
   <location>
-    <uid>jupiter</uid>
-    <guid>jupiter</guid>
-    <password>test</password>
-    <name>Jupiter Conference Room, Building 2, 1st Floor</name>
+    <uid>sharissroom</uid>
+    <guid>80689D41-DAF8-4189-909C-DB017B271892</guid>
+    <name>Shari's Room</name>
   </location>
   <location>
-    <uid>neptune</uid>
-    <guid>neptune</guid>
-    <password>test</password>
-    <name>Neptune Conference Room, Building 2, 1st Floor</name>
-  </location>
-  <location>
     <uid>pluto</uid>
     <guid>pluto</guid>
-    <password>test</password>
     <name>Pluto Conference Room, Building 2, 1st Floor</name>
   </location>
   <location>
     <uid>saturn</uid>
     <guid>saturn</guid>
-    <password>test</password>
     <name>Saturn Conference Room, Building 2, 1st Floor</name>
   </location>
   <location>
-    <uid>uranus</uid>
-    <guid>uranus</guid>
-    <password>test</password>
-    <name>Uranus Conference Room, Building 3, 1st Floor</name>
+    <uid>location10</uid>
+    <guid>location10</guid>
+    <name>Room 10</name>
   </location>
   <location>
-    <uid>delegatedroom</uid>
-    <guid>delegatedroom</guid>
-    <password>delegatedroom</password>
-    <name>Delegated Conference Room</name>
+    <uid>neptune</uid>
+    <guid>neptune</guid>
+    <name>Neptune Conference Room, Building 2, 1st Floor</name>
   </location>
+  <location>
+    <uid>Earth</uid>
+    <guid>Earth</guid>
+    <name>Earth Conference Room, Building 1, 1st Floor</name>
+  </location>
+  <location>
+    <uid>venus</uid>
+    <guid>venus</guid>
+    <name>Venus Conference Room, Building 1, 2nd Floor</name>
+  </location>
+  <resource>
+    <uid>sharisotherresource</uid>
+    <guid>CCE95217-A57B-481A-AC3D-FEC9AB6CE3A9</guid>
+    <name>Shari's Other Resource</name>
+  </resource>
+  <resource>
+    <uid>resource15</uid>
+    <guid>resource15</guid>
+    <name>Resource 15</name>
+  </resource>
+  <resource>
+    <uid>resource14</uid>
+    <guid>resource14</guid>
+    <name>Resource 14</name>
+  </resource>
+  <resource>
+    <uid>resource17</uid>
+    <guid>resource17</guid>
+    <name>Resource 17</name>
+  </resource>
+  <resource>
+    <uid>resource16</uid>
+    <guid>resource16</guid>
+    <name>Resource 16</name>
+  </resource>
+  <resource>
+    <uid>resource11</uid>
+    <guid>resource11</guid>
+    <name>Resource 11</name>
+  </resource>
+  <resource>
+    <uid>resource10</uid>
+    <guid>resource10</guid>
+    <name>Resource 10</name>
+  </resource>
+  <resource>
+    <uid>resource13</uid>
+    <guid>resource13</guid>
+    <name>Resource 13</name>
+  </resource>
+  <resource>
+    <uid>resource12</uid>
+    <guid>resource12</guid>
+    <name>Resource 12</name>
+  </resource>
+  <resource>
+    <uid>resource19</uid>
+    <guid>resource19</guid>
+    <name>Resource 19</name>
+  </resource>
+  <resource>
+    <uid>resource18</uid>
+    <guid>resource18</guid>
+    <name>Resource 18</name>
+  </resource>
+  <resource>
+    <uid>sharisresource</uid>
+    <guid>C38BEE7A-36EE-478C-9DCB-CBF4612AFE65</guid>
+    <name>Shari's Resource</name>
+  </resource>
+  <resource>
+    <uid>resource20</uid>
+    <guid>resource20</guid>
+    <name>Resource 20</name>
+  </resource>
+  <resource>
+    <uid>resource06</uid>
+    <guid>resource06</guid>
+    <name>Resource 06</name>
+  </resource>
+  <resource>
+    <uid>resource07</uid>
+    <guid>resource07</guid>
+    <name>Resource 07</name>
+  </resource>
+  <resource>
+    <uid>resource04</uid>
+    <guid>resource04</guid>
+    <name>Resource 04</name>
+  </resource>
+  <resource>
+    <uid>resource05</uid>
+    <guid>resource05</guid>
+    <name>Resource 05</name>
+  </resource>
+  <resource>
+    <uid>resource02</uid>
+    <guid>resource02</guid>
+    <name>Resource 02</name>
+  </resource>
+  <resource>
+    <uid>resource03</uid>
+    <guid>resource03</guid>
+    <name>Resource 03</name>
+  </resource>
+  <resource>
+    <uid>resource01</uid>
+    <guid>resource01</guid>
+    <name>Resource 01</name>
+  </resource>
+  <resource>
+    <uid>sharisotherresource1</uid>
+    <guid>0CE0BF31-5F9E-4801-A489-8C70CF287F5F</guid>
+    <name>Shari's Other Resource1</name>
+  </resource>
+  <resource>
+    <uid>resource08</uid>
+    <guid>resource08</guid>
+    <name>Resource 08</name>
+  </resource>
+  <resource>
+    <uid>resource09</uid>
+    <guid>resource09</guid>
+    <name>Resource 09</name>
+  </resource>
 </accounts>


Property changes on: CalendarServer/trunk/conf/auth/resources-test.xml
___________________________________________________________________
Added: svn:executable
   + *

Modified: CalendarServer/trunk/twext/enterprise/queue.py
===================================================================
--- CalendarServer/trunk/twext/enterprise/queue.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/twext/enterprise/queue.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -87,7 +87,7 @@
 from twisted.application.service import MultiService
 from twisted.internet.protocol import Factory
 from twisted.internet.defer import (
-    inlineCallbacks, returnValue, Deferred, passthru
+    inlineCallbacks, returnValue, Deferred, passthru, succeed
 )
 from twisted.internet.endpoints import TCP4ClientEndpoint
 from twisted.protocols.amp import AMP, Command, Integer, Argument, String
@@ -865,6 +865,9 @@
 
 
 
+
+
+
 class WorkerFactory(Factory, object):
     """
     Factory, to be used as the client to connect from the worker to the
@@ -1446,4 +1449,41 @@
         """
         Choose to perform the work locally.
         """
-        return LocalPerformer(self.txnFactory)
\ No newline at end of file
+        return LocalPerformer(self.txnFactory)
+
+
+
+class NonPerformer(object):
+    """
+    Implementor of C{performWork} that doesn't actual perform any work.  This
+    is used in the case where you want to be able to enqueue work for someone
+    else to do, but not take on any work yourself (such as a command line tool).
+    """
+    implements(_IWorkPerformer)
+
+    def performWork(self, table, workID):
+        """
+        Don't perform work.
+        """
+        return succeed(None)
+
+
+class NonPerformingQueuer(_BaseQueuer):
+    """
+    When work is enqueued with this queuer, it is never executed locally.
+    It's expected that the polling machinery will find the work and perform it.
+    """
+    implements(IQueuer)
+
+    def __init__(self, reactor=None):
+        super(NonPerformingQueuer, self).__init__()
+        if reactor is None:
+            from twisted.internet import reactor
+        self.reactor = reactor
+
+
+    def choosePerformer(self):
+        """
+        Choose to perform the work locally.
+        """
+        return NonPerformer()
\ No newline at end of file

Modified: CalendarServer/trunk/twext/enterprise/test/test_queue.py
===================================================================
--- CalendarServer/trunk/twext/enterprise/test/test_queue.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/twext/enterprise/test/test_queue.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -54,7 +54,7 @@
 from zope.interface.verify import verifyObject
 from twisted.test.proto_helpers import StringTransport
 
-from twext.enterprise.queue import _BaseQueuer
+from twext.enterprise.queue import _BaseQueuer, NonPerformingQueuer
 import twext.enterprise.queue
 
 class Clock(_Clock):
@@ -654,3 +654,14 @@
         queuer.enqueueWork(None, None)
         self.assertNotEqual(self.proposal, None)
 
+
+class NonPerformingQueuerTests(TestCase):
+
+    @inlineCallbacks
+    def test_choosePerformer(self):
+        queuer = NonPerformingQueuer()
+        performer = queuer.choosePerformer()
+        result = (yield performer.performWork(None, None))
+        self.assertEquals(result, None)
+
+

Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -594,7 +594,8 @@
 
         @param principalUID: the UID of the principal to remove.
         """
-
+        # FIXME: This method doesn't appear to be used anywhere.  Still needed?
+        
         if delay:
             # We are going to remove the principal only after <delay> seconds
             # has passed since we first chose to remove it, to protect against

Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -49,7 +49,6 @@
 from twext.python.log import Logger, LoggingMixIn
 
 from twistedcaldav.config import config
-from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 
 from twistedcaldav.directory.idirectory import IDirectoryService, IDirectoryRecord
 from twistedcaldav.directory.util import uuidFromName, normalizeUUID
@@ -935,7 +934,7 @@
         # Delete all other work items
         yield Delete(From=self.table, Where=None).on(self.transaction)
 
-        groupCacher = self.transaction._groupCacher
+        groupCacher = getattr(self.transaction, "_groupCacher", None)
         if groupCacher is not None:
             try:
                 yield groupCacher.updateCache()
@@ -947,6 +946,12 @@
                 log.debug("Scheduling next group cacher update: %s" % (notBefore,))
                 yield self.transaction.enqueue(GroupCacherPollingWork,
                     notBefore=notBefore)
+        else:
+            notBefore = (datetime.datetime.utcnow() +
+                datetime.timedelta(seconds=10))
+            log.debug("Rescheduling group cacher update: %s" % (notBefore,))
+            yield self.transaction.enqueue(GroupCacherPollingWork,
+                notBefore=notBefore)
 
 
 @inlineCallbacks

Modified: CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_inbound.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_inbound.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/test/test_inbound.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -15,6 +15,7 @@
 ##
 
 
+from twistedcaldav.test.util import TestCase
 import email
 from twisted.internet.defer import inlineCallbacks
 from twisted.python.modules import getModule
@@ -24,7 +25,6 @@
 from twistedcaldav.scheduling.imip.inbound import injectMessage
 from twistedcaldav.scheduling.imip.inbound import IMIPReplyWork
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.test.util import TestCase
 from twistedcaldav.test.util import xmlFile
 from txdav.common.datastore.test.util import buildStore
 from calendarserver.tap.util import getRootResource

Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py	2013-04-03 01:26:07 UTC (rev 10981)
+++ CalendarServer/trunk/twistedcaldav/test/util.py	2013-04-03 01:26:13 UTC (rev 10982)
@@ -19,7 +19,7 @@
 import os
 import xattr
 
-from calendarserver.provision.root import RootResource
+from twistedcaldav.stdconfig import config
 
 from twisted.python.failure import Failure
 from twisted.internet.base import DelayedCall
@@ -34,7 +34,6 @@
 
 from twistedcaldav import memcacher
 from twistedcaldav.bind import doBind
-from twistedcaldav.config import config
 from twistedcaldav.directory import augment
 from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
 from twistedcaldav.directory.calendar import (
@@ -48,6 +47,8 @@
 from txdav.common.datastore.test.util import deriveQuota
 from txdav.common.datastore.file import CommonDataStore
 
+from calendarserver.provision.root import RootResource
+
 from twext.python.log import Logger
 
 log = Logger()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130402/e562e799/attachment-0001.html>


More information about the calendarserver-changes mailing list