[CalendarServer-changes] [4958] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Jan 25 07:56:35 PST 2010


Revision: 4958
          http://trac.macosforge.org/projects/calendarserver/changeset/4958
Author:   cdaboo at apple.com
Date:     2010-01-25 07:56:33 -0800 (Mon, 25 Jan 2010)
Log Message:
-----------
Merged partition branch to trunk.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/provision/root.py
    CalendarServer/trunk/calendarserver/sidecar/task.py
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/calendarserver/tap/test/test_caldav.py
    CalendarServer/trunk/calendarserver/tools/principals.py
    CalendarServer/trunk/calendarserver/tools/util.py
    CalendarServer/trunk/calendarserver/webadmin/resource.py
    CalendarServer/trunk/conf/auth/accounts-test.xml
    CalendarServer/trunk/conf/auth/accounts.dtd
    CalendarServer/trunk/conf/auth/accounts.xml
    CalendarServer/trunk/conf/caldavd-apple.plist
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/conf/caldavd.plist
    CalendarServer/trunk/contrib/migration/59_calendarmigrator.py
    CalendarServer/trunk/memcacheclient.py
    CalendarServer/trunk/support/build.sh
    CalendarServer/trunk/twistedcaldav/cache.py
    CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/trunk/twistedcaldav/directory/cachingdirectory.py
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/trunk/twistedcaldav/directory/digest.py
    CalendarServer/trunk/twistedcaldav/directory/directory.py
    CalendarServer/trunk/twistedcaldav/directory/idirectory.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/directory/sudo.py
    CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml
    CalendarServer/trunk/twistedcaldav/directory/test/test_aggregate.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_cachedirectory.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_calendar.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_guidchange.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipaldb.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_xmlfile.py
    CalendarServer/trunk/twistedcaldav/directory/xmlaccountsparser.py
    CalendarServer/trunk/twistedcaldav/directory/xmlfile.py
    CalendarServer/trunk/twistedcaldav/extensions.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/index.py
    CalendarServer/trunk/twistedcaldav/log.py
    CalendarServer/trunk/twistedcaldav/mail.py
    CalendarServer/trunk/twistedcaldav/memcachepool.py
    CalendarServer/trunk/twistedcaldav/memcacheprops.py
    CalendarServer/trunk/twistedcaldav/memcacher.py
    CalendarServer/trunk/twistedcaldav/method/report_common.py
    CalendarServer/trunk/twistedcaldav/notify.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
    CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py
    CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py
    CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/trunk/twistedcaldav/scheduling/processing.py
    CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py
    CalendarServer/trunk/twistedcaldav/scheduling/utils.py
    CalendarServer/trunk/twistedcaldav/static.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/test/test_cache.py
    CalendarServer/trunk/twistedcaldav/test/test_config.py
    CalendarServer/trunk/twistedcaldav/test/test_upgrade.py
    CalendarServer/trunk/twistedcaldav/test/util.py
    CalendarServer/trunk/twistedcaldav/upgrade.py

Added Paths:
-----------
    CalendarServer/trunk/bin/calendarserver_load_augmentdb
    CalendarServer/trunk/bin/calendarserver_manage_augments
    CalendarServer/trunk/calendarserver/tools/loadaugmentdb.py
    CalendarServer/trunk/calendarserver/tools/manageaugments.py
    CalendarServer/trunk/calendarserver/tools/managepostgres.py
    CalendarServer/trunk/conf/auth/augments-test.xml
    CalendarServer/trunk/conf/auth/augments.dtd
    CalendarServer/trunk/conf/auth/proxies-test.xml
    CalendarServer/trunk/conf/auth/proxies.dtd
    CalendarServer/trunk/conf/partitions-test.plist
    CalendarServer/trunk/conf/partitions.plist
    CalendarServer/trunk/twistedcaldav/client/
    CalendarServer/trunk/twistedcaldav/client/__init__.py
    CalendarServer/trunk/twistedcaldav/client/pool.py
    CalendarServer/trunk/twistedcaldav/client/reverseproxy.py
    CalendarServer/trunk/twistedcaldav/database.py
    CalendarServer/trunk/twistedcaldav/directory/augment.py
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxyloader.py
    CalendarServer/trunk/twistedcaldav/directory/test/augments-test-default.xml
    CalendarServer/trunk/twistedcaldav/directory/test/augments-test.xml
    CalendarServer/trunk/twistedcaldav/directory/test/augments.xml
    CalendarServer/trunk/twistedcaldav/directory/test/proxies.xml
    CalendarServer/trunk/twistedcaldav/directory/test/test_augment.py
    CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py
    CalendarServer/trunk/twistedcaldav/partitions.py
    CalendarServer/trunk/twistedcaldav/test/test_database.py

Removed Paths:
-------------
    CalendarServer/trunk/conf/auth/accounts.htauth
    CalendarServer/trunk/conf/auth/accounts.htdigest
    CalendarServer/trunk/conf/auth/groups
    CalendarServer/trunk/twistedcaldav/client/__init__.py
    CalendarServer/trunk/twistedcaldav/client/pool.py
    CalendarServer/trunk/twistedcaldav/client/reverseproxy.py
    CalendarServer/trunk/twistedcaldav/directory/apache.py
    CalendarServer/trunk/twistedcaldav/directory/sqldb.py
    CalendarServer/trunk/twistedcaldav/directory/test/basic
    CalendarServer/trunk/twistedcaldav/directory/test/digest
    CalendarServer/trunk/twistedcaldav/directory/test/groups
    CalendarServer/trunk/twistedcaldav/directory/test/test_apache.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryschema.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_sqldb.py

Property Changed:
----------------
    CalendarServer/trunk/
    CalendarServer/trunk/doc/Extensions/caldav-privatecomments.txt
    CalendarServer/trunk/doc/Extensions/caldav-privatecomments.xml


Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/more-deferreds:4446,4462
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
Deleted: svk:merge
   - e27351fd-9f3e-4f54-a53b-843176b1656c:/CalendarServer/branches/more-deferreds:4462


Copied: CalendarServer/trunk/bin/calendarserver_load_augmentdb (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/bin/calendarserver_load_augmentdb)
===================================================================
--- CalendarServer/trunk/bin/calendarserver_load_augmentdb	                        (rev 0)
+++ CalendarServer/trunk/bin/calendarserver_load_augmentdb	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2010 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.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+    from calendarserver.tools.loadaugmentdb import main
+    main()

Copied: CalendarServer/trunk/bin/calendarserver_manage_augments (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/bin/calendarserver_manage_augments)
===================================================================
--- CalendarServer/trunk/bin/calendarserver_manage_augments	                        (rev 0)
+++ CalendarServer/trunk/bin/calendarserver_manage_augments	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2010 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.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+    from calendarserver.tools.manageaugments import main
+    main()

Modified: CalendarServer/trunk/calendarserver/provision/root.py
===================================================================
--- CalendarServer/trunk/calendarserver/provision/root.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/calendarserver/provision/root.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -61,7 +61,7 @@
 
         self.contentFilters = []
 
-        if config.Memcached["ClientEnabled"]:
+        if config.Memcached.Pools.Default.ClientEnabled:
             self.responseCache = MemcacheResponseCache(self.fp)
 
             CalendarHomeFile.cacheNotifierFactory = MemcacheChangeNotifier
@@ -244,6 +244,15 @@
                             "Your client software (%s) is not allowed to access this service." % (agent,)
                         ))
 
+        # Look for forwarding
+        if config.Partitioning.Enabled:
+            remote_ip = request.headers.getRawHeaders('x-forwarded-for')
+            if remote_ip and len(remote_ip) == 1:
+                request.forwarded_for = remote_ip[0]
+                if not hasattr(request, "extendedLogItems"):
+                    request.extendedLogItems = {}
+                request.extendedLogItems["xff"] = remote_ip[0]
+
         if request.method == "PROPFIND" and not getattr(request, "notInCache", False) and len(segments) > 1:
             try:
                 authnUser, authzUser = (yield self.authenticate(request))

Modified: CalendarServer/trunk/calendarserver/sidecar/task.py
===================================================================
--- CalendarServer/trunk/calendarserver/sidecar/task.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/calendarserver/sidecar/task.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,4 +1,4 @@
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -379,15 +379,10 @@
         #
         # Configure Memcached Client Pool
         #
-        if config.Memcached.ClientEnabled:
-            memcachepool.installPool(
-                IPv4Address(
-                    "TCP",
-                    config.Memcached.BindAddress,
-                    config.Memcached.Port,
-                ),
-                config.Memcached.MaxClients,
-            )
+        memcachepool.installPools(
+            config.Memcached.Pools,
+            config.Memcached.MaxClients,
+        )
 
         #
         # Configure NotificationClient

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,6 +1,6 @@
 # -*- test-case-name: calendarserver.tap.test.test_caldav -*-
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -39,6 +39,7 @@
 from twisted.python.usage import Options, UsageError
 from twisted.python.reflect import namedClass
 from twisted.plugin import IPlugin
+from twisted.internet import reactor
 from twisted.internet.reactor import callLater, spawnProcess
 from twisted.internet.process import ProcessExitedAlready
 from twisted.internet.protocol import Protocol, Factory
@@ -64,32 +65,35 @@
     sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "support"))
     from version import version as getVersion
     version = "%s (%s)" % getVersion()
-from twistedcaldav.log import Logger, LoggingMixIn
-from twistedcaldav.log import logLevelForNamespace, setLogLevelForNamespace
+from twistedcaldav import memcachepool
+from twistedcaldav.accesslog import AMPCommonAccessLoggingObserver
+from twistedcaldav.accesslog import AMPLoggingFactory
 from twistedcaldav.accesslog import DirectoryLogWrapperResource
 from twistedcaldav.accesslog import RotatingFileAccessLoggingObserver
-from twistedcaldav.accesslog import AMPLoggingFactory
-from twistedcaldav.accesslog import AMPCommonAccessLoggingObserver
-from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
+from twistedcaldav.config import ConfigurationError
 from twistedcaldav.config import config
-from twistedcaldav.config import ConfigurationError
-from twistedcaldav.resource import CalDAVResource, AuthenticationWrapper
+from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory.aggregate import AggregateDirectoryService
+from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
 from twistedcaldav.directory.digest import QopDigestCredentialFactory
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.sudo import SudoDirectoryService
 from twistedcaldav.directory.util import NotFilePath
 from twistedcaldav.directory.wiki import WikiDirectoryService
+from twistedcaldav.localization import processLocalizationFiles
+from twistedcaldav.log import Logger, LoggingMixIn
+from twistedcaldav.log import logLevelForNamespace, setLogLevelForNamespace
+from twistedcaldav.mail import IMIPReplyInboxResource
+from twistedcaldav.notify import installNotificationClient
+from twistedcaldav.pdmonster import PDClientAddressWrapper
+from twistedcaldav.resource import CalDAVResource, AuthenticationWrapper
 from twistedcaldav.static import CalendarHomeProvisioningFile
 from twistedcaldav.static import IScheduleInboxFile
 from twistedcaldav.static import TimezoneServiceFile
-from twistedcaldav.mail import IMIPReplyInboxResource
+from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav.upgrade import upgradeData
-from twistedcaldav import memcachepool
-from twistedcaldav.notify import installNotificationClient
 from twistedcaldav.util import getNCPU
-from twistedcaldav.localization import processLocalizationFiles
 
 try:
     from twistedcaldav.authkerb import NegotiateCredentialFactory
@@ -409,10 +413,10 @@
                 # Now do any on disk upgrades we might need.
                 # Memcache isn't running at this point, so temporarily change
                 # the config so nobody tries to talk to it while upgrading
-                memcacheSetting = config.Memcached.ClientEnabled
-                config.Memcached.ClientEnabled = False
+                memcacheSetting = config.Memcached.Pools.Default.ClientEnabled
+                config.Memcached.Pools.Default.ClientEnabled = False
                 upgradeData(config)
-                config.Memcached.ClientEnabled = memcacheSetting
+                config.Memcached.Pools.Default.ClientEnabled = memcacheSetting
 
 
             service = serviceMethod(options)
@@ -517,17 +521,48 @@
                 SudoDirectoryService.recordType_sudoers)
 
         #
+        # Setup the Augment Service
+        #
+        augmentClass = namedClass(config.AugmentService.type)
+
+        self.log_info("Configuring augment service of type: %s" % (augmentClass,))
+
+        try:
+            augment.AugmentService = augmentClass(**config.AugmentService.params)
+        except IOError, e:
+            self.log_error("Could not start augment service")
+            raise
+
+        #
+        # Setup the PoxyDB Service
+        #
+        proxydbClass = namedClass(config.ProxyDBService.type)
+
+        self.log_info("Configuring proxydb service of type: %s" % (proxydbClass,))
+
+        try:
+            calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
+        except IOError, e:
+            self.log_error("Could not start proxydb service")
+            raise
+
+        #
+        # Make sure proxies get initialized
+        #
+        if config.ProxyLoadFromFile:
+            def _doProxyUpdate():
+                loader = XMLCalendarUserProxyLoader(config.ProxyLoadFromFile)
+                return loader.updateProxyDB()
+            
+            reactor.addSystemEventTrigger("after", "startup", _doProxyUpdate)
+
+        #
         # Configure Memcached Client Pool
         #
-        if config.Memcached.ClientEnabled:
-            memcachepool.installPool(
-                IPv4Address(
-                    "TCP",
-                    config.Memcached.BindAddress,
-                    config.Memcached.Port,
-                ),
-                config.Memcached.MaxClients,
-            )
+        memcachepool.installPools(
+            config.Memcached.Pools,
+            config.Memcached.MaxClients,
+        )
 
         #
         # Configure NotificationClient
@@ -1037,26 +1072,24 @@
 
 
 
-        if config.Memcached.ServerEnabled:
-            self.log_info("Adding memcached service")
+        for name, pool in config.Memcached.Pools.items():
+            if pool.ServerEnabled:
+                self.log_info("Adding memcached service for pool: %s" % (name,))
+        
+                memcachedArgv = [
+                    config.Memcached.memcached,
+                    "-p", str(pool.Port),
+                    "-l", pool.BindAddress,
+                    "-U", "0",
+                ]
+        
+                if config.Memcached.MaxMemory is not 0:
+                    memcachedArgv.extend(["-m", str(config.Memcached.MaxMemory)])
+        
+                memcachedArgv.extend(config.Memcached.Options)
+        
+                monitor.addProcess('memcached-%s' % (name,), memcachedArgv, env=parentEnv)
 
-            memcachedArgv = [
-                config.Memcached.memcached,
-                "-p", str(config.Memcached.Port),
-                "-l", config.Memcached.BindAddress,
-                "-U", "0",
-            ]
-
-            if config.Memcached.MaxMemory is not 0:
-                memcachedArgv.extend(["-m", str(config.Memcached.MaxMemory)])
-
-            if config.UserName:
-                memcachedArgv.extend(["-u", config.UserName])
-
-            memcachedArgv.extend(config.Memcached.Options)
-
-            monitor.addProcess("memcached", memcachedArgv, env=parentEnv)
-
         if (
             config.Notifications.Enabled and
             config.Notifications.InternalNotificationHost == "localhost"
@@ -1212,9 +1245,6 @@
                   % (config.ControlPort,),
         ])
 
-        if config.Memcached.ServerEnabled:
-            args.extend(["-o", "Memcached/ClientEnabled=True"])
-
         if self.inheritFDs:
             args.extend([
                 "-o", "InheritFDs=%s" % (",".join(map(str, self.inheritFDs)),)

Modified: CalendarServer/trunk/calendarserver/tap/test/test_caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/test/test_caldav.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/calendarserver/tap/test/test_caldav.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2007 Apple Inc. All rights reserved.
+# Copyright (c) 2007-2010 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.
@@ -185,6 +185,7 @@
         self.config = ConfigDict(DEFAULT_CONFIG)
 
         accountsFile = os.path.join(sourceRoot, "twistedcaldav/directory/test/accounts.xml")
+        augmentsFile = os.path.join(sourceRoot, "twistedcaldav/directory/test/augments.xml")
         pemFile = os.path.join(sourceRoot, "twistedcaldav/test/data/server.pem")
 
         self.config["DirectoryService"] = {
@@ -192,6 +193,11 @@
             "type": "twistedcaldav.directory.xmlfile.XMLDirectoryService"
         }
 
+        self.config["AugmentService"] = {
+            "params": {"xmlFiles": [augmentsFile]},
+            "type": "twistedcaldav.directory.augment.AugmentXMLDB"
+        }
+
         self.config.DocumentRoot   = self.mktemp()
         self.config.DataRoot       = self.mktemp()
         self.config.ProcessType    = "Slave"

Copied: CalendarServer/trunk/calendarserver/tools/loadaugmentdb.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/calendarserver/tools/loadaugmentdb.py)
===================================================================
--- CalendarServer/trunk/calendarserver/tools/loadaugmentdb.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/loadaugmentdb.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from calendarserver.tools.util import loadConfig, getDirectory,\
+    autoDisableMemcached
+from getopt import getopt, GetoptError
+from grp import getgrnam
+from pwd import getpwnam
+from sys import stdout, stderr
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.log import addObserver, removeObserver
+from twisted.python.util import switchUID
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.directory import augment
+from twistedcaldav.directory.augment import AugmentXMLDB
+from twistedcaldav.log import setLogLevelForNamespace
+import os
+import sys
+
+class UsageError (StandardError):
+    pass
+
+class StandardIOObserver (object):
+    """
+    Log observer that writes to standard I/O.
+    """
+    def emit(self, eventDict):
+        text = None
+
+        if eventDict["isError"]:
+            output = stderr
+            if "failure" in eventDict:
+                text = eventDict["failure"].getTraceback()
+        else:
+            output = stdout
+
+        if not text:
+            text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
+
+        output.write(text)
+        output.flush()
+
+    def start(self):
+        addObserver(self.emit)
+
+    def stop(self):
+        removeObserver(self.emit)
+
+def usage(e=None):
+    if e:
+        print e
+        print ""
+
+    name = os.path.basename(sys.argv[0])
+    print "usage: %s [options]" % (name,)
+    print ""
+    print "Populate an sqlite or PostgreSQL augments database with values"
+    print "from an XML augments file."
+    print ""
+    print "options:"
+    print "  -h --help: print this help and exit"
+    print "  -f --config: Specify caldavd.plist configuration path"
+    print "  -x --xmlfile: Specify xml augments file path"
+    print "  -r --remove: Remove all entries from the database"
+
+    if e:
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+def main():
+    try:
+        (optargs, args) = getopt(
+            sys.argv[1:], "hf:rx:", [
+                "config=",
+                "remove",
+                "xmlfile=",
+                "help",
+            ],
+        )
+    except GetoptError, e:
+        usage(e)
+
+    configFileName = None
+    xmlFileName = None
+    remove = False
+
+    for opt, arg in optargs:
+        if opt in ("-h", "--help"):
+            usage()
+
+        elif opt in ("-f", "--config"):
+            configFileName = arg
+
+        elif opt in ("-r", "--remove"):
+            remove = True
+            if raw_input("Do you really want to remove all records from the database? [y/n] ") != "y":
+                sys.exit(0)
+
+        elif opt in ("-x", "--xmlfile"):
+            xmlFileName = arg
+
+    if args:
+        usage("Too many arguments: %s" % (" ".join(args),))
+
+    observer = StandardIOObserver()
+    observer.start()
+
+    #
+    # Get configuration
+    #
+    try:
+        loadConfig(configFileName)
+        setLogLevelForNamespace(None, "warn")
+
+        # Shed privileges
+        if config.UserName and config.GroupName and os.getuid() == 0:
+            uid = getpwnam(config.UserName).pw_uid
+            gid = getgrnam(config.GroupName).gr_gid
+            switchUID(uid, uid, gid)
+
+        os.umask(config.umask)
+
+        config.directory = getDirectory()
+        autoDisableMemcached(config)
+    except ConfigurationError, e:
+        usage("Unable to start: %s" % (e,))
+
+    try:
+        dbxml = AugmentXMLDB((xmlFileName,)) if not remove else None
+    except IOError, e:
+        usage("Could not read XML augment file: %s" % (e,))
+
+    #
+    # Start the reactor
+    #
+    reactor.callLater(0, run, dbxml)
+    reactor.run()
+
+ at inlineCallbacks
+def run(dbxml):
+    
+    try:
+        guids = set((yield augment.AugmentService.getAllGUIDs()))
+        added = 0
+        updated = 0
+        removed = 0
+        if dbxml:
+            for record in dbxml.db.values():
+                yield augment.AugmentService.addAugmentRecord(record, record.guid in guids)
+                if record.guid in guids:
+                    updated += 1
+                else:
+                    added += 1
+            for guid in guids.difference(dbxml.db.keys()):
+                yield augment.AugmentService.removeAugmentRecord(guid)
+                removed += 1
+        else:
+            yield augment.AugmentService.clean()
+            removed = len(guids)
+            
+        print "Changes:"
+        print "  Added: %d" % (added,)
+        print "  Changed: %d" % (updated,)
+        print "  Removed: %d" % (removed,)
+    finally:
+        #
+        # Stop the reactor
+        #
+        reactor.stop()

Copied: CalendarServer/trunk/calendarserver/tools/manageaugments.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/calendarserver/tools/manageaugments.py)
===================================================================
--- CalendarServer/trunk/calendarserver/tools/manageaugments.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/manageaugments.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,200 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from optparse import OptionParser
+from twistedcaldav.directory import xmlaugmentsparser
+from xml.etree.ElementTree import ElementTree, tostring, SubElement
+from xml.parsers.expat import ExpatError
+import sys
+
+def error(s):
+    print s
+    sys.exit(1)
+
+def readXML(xmlfile):
+
+    # Read in XML
+    try:
+        tree = ElementTree(file=xmlfile)
+    except ExpatError, e:
+        error("Unable to parse file '%s' because: %s" % (xmlfile, e,))
+
+    # Verify that top-level element is correct
+    augments_node = tree.getroot()
+    if augments_node.tag != xmlaugmentsparser.ELEMENT_AUGMENTS:
+        error("Ignoring file '%s' because it is not a augments file" % (xmlfile,))
+
+    return augments_node
+
+def writeXML(xmlfile, root):
+    
+    data = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE augments SYSTEM "augments.dtd">
+
+""" + tostring(root)
+
+    with open(xmlfile, "w") as f:
+        f.write(data)
+
+def addSubElement(parent, tag, text=None, indent=0):
+    
+    child = SubElement(parent, tag)
+    child.text = text
+    child.tail = "\n" + " " * indent
+    return child
+
+def changeSubElementText(parent, tag, text):
+    
+    child = parent.find(tag)
+    child.text = text
+
+def doAdd(xmlfile, guid, host, enable_calendar, auto_schedule):
+
+    augments_node = readXML(xmlfile)
+
+    # Make sure GUID is not already present
+    for child in augments_node.getchildren():
+        
+        if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
+            error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
+
+        for node in child.getchildren():
+            
+            if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
+                error("Cannot add guid '%s' because it already exists in augment file: '%s'" % (guid, xmlfile,))
+    
+    # Create new record
+    augments_node.getchildren()[-1].tail = "\n  "
+    record = addSubElement(augments_node, xmlaugmentsparser.ELEMENT_RECORD, "\n    ")
+    addSubElement(record, xmlaugmentsparser.ELEMENT_GUID, guid, 4)
+    addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLE, "true", 4)
+    addSubElement(record, xmlaugmentsparser.ELEMENT_HOSTEDAT, host, 4)
+    addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLECALENDAR, "true" if enable_calendar else "false", 4)
+    addSubElement(record, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE, "true" if auto_schedule else "false", 2)
+    
+    # Modify xmlfile
+    writeXML(xmlfile, augments_node)
+    print "Added guid '%s' in augment file: '%s'" % (guid, xmlfile,)
+    
+def doModify(xmlfile, guid, host, enable_calendar, auto_schedule):
+
+    augments_node = readXML(xmlfile)
+
+    # Make sure GUID is present
+    for child in augments_node.getchildren():
+        
+        if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
+            error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
+
+        for node in child.getchildren():
+            
+            if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
+                break
+        else:
+            continue
+        break
+    else:
+        error("Cannot modify guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
+    
+    # Modify record
+    if host is not None:
+        child.find(xmlaugmentsparser.ELEMENT_HOSTEDAT).text = host
+    child.find(xmlaugmentsparser.ELEMENT_ENABLECALENDAR).text = "true" if enable_calendar else "false"
+    child.find(xmlaugmentsparser.ELEMENT_AUTOSCHEDULE).text = "true" if auto_schedule else "false"
+    
+    # Modify xmlfile
+    writeXML(xmlfile, augments_node)
+    print "Modified guid '%s' in augment file: '%s'" % (guid, xmlfile,)
+
+def doRemove(xmlfile, guid):
+
+    augments_node = readXML(xmlfile)
+
+    # Make sure GUID is present
+    for child in augments_node.getchildren():
+        
+        if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
+            error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
+
+        for node in child.getchildren():
+            
+            if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
+                break
+        else:
+            continue
+        augments_node.remove(child)
+        break
+    else:
+        error("Cannot remove guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
+    
+    # Modify xmlfile
+    writeXML(xmlfile, augments_node)
+    print "Removed guid '%s' from augment file: '%s'" % (guid, xmlfile,)
+    
+def doPrint(xmlfile):
+
+    # Read in XML
+    augments_node = readXML(xmlfile)
+
+    print tostring(augments_node)
+
+def main():
+
+    usage = "%prog [options] ACTION"
+    epilog = """
+ACTION is one of add|modify|remove|print
+
+  add:    add a user record
+  modify: modify a user record
+  remove: remove a user record
+  print:  print all user records
+"""
+    description = "Tool to manipulate CalendarServer augments XML file"
+    version = "%prog v1.0"
+    parser = OptionParser(usage=usage, description=description, version=version)
+    parser.epilog = epilog
+    parser.format_epilog = lambda _:epilog
+
+    parser.add_option("-f", "--file", dest="xmlfilename",
+                      help="XML augment file to manipulate", metavar="FILE")
+    parser.add_option("-g", "--guid", dest="guid",
+                      help="OD GUID to manipulate", metavar="GUID")
+    parser.add_option("-n", "--node", dest="node",
+                      help="Partition node to assign to GUID", metavar="NODE")
+    parser.add_option("-c", "--enable-calendar", action="store_true", dest="enable_calendar",
+                      default=True, help="Enable calendaring for this GUID: %default")
+    parser.add_option("-a", "--auto-schedule", action="store_true", dest="auto_schedule",
+                      default=False, help="Enable auto-schedule for this GUID: %default")
+
+    (options, args) = parser.parse_args()
+
+    if len(args) != 1:
+        parser.error("incorrect number of arguments")
+
+    if args[0] == "add":
+        if not options.node:
+            parser.error("Partition node must be specified when adding")   
+        doAdd(options.xmlfilename, options.guid, options.node, options.enable_calendar, options.auto_schedule)
+    elif args[0] == "modify":
+        doModify(options.xmlfilename, options.guid, options.node, options.enable_calendar, options.auto_schedule)
+    elif args[0] == "remove":
+        doRemove(options.xmlfilename, options.guid)
+    elif args[0] == "print":
+        doPrint(options.xmlfilename)
+
+if __name__ == '__main__':
+    main()

Copied: CalendarServer/trunk/calendarserver/tools/managepostgres.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/calendarserver/tools/managepostgres.py)
===================================================================
--- CalendarServer/trunk/calendarserver/tools/managepostgres.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/managepostgres.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from optparse import OptionParser
+import os
+import subprocess
+import sys
+import time
+
+def error(s):
+    print s
+    sys.exit(1)
+
+def cmd(s):
+    print s
+    subprocess.call(s, shell=True)
+
+def doInit(basedir):
+    
+    cmd("mkdir %s/data" % (basedir,))
+    cmd("%s/bin/initdb -D %s/data" % (basedir, basedir,))
+    
+    # Have the DB listen on all interfaces
+    with open("%s/data/postgresql.conf" % (basedir,)) as f:
+        conf = f.read()
+    conf = conf.replace("#listen_addresses = 'localhost'", "listen_addresses = '*'\t")
+    conf = conf.replace("max_connections = 20 ", "max_connections = 500")
+    with open("%s/data/postgresql.conf" % (basedir,), "w") as f:
+        f.write(conf)
+        
+    # Allow current user to auth to the DBs
+    with open("%s/data/pg_hba.conf" % (basedir,)) as f:
+        conf = f.read()
+    conf = conf.replace("127.0.0.1/32", "0.0.0.0/0   ")
+    with open("%s/data/pg_hba.conf" % (basedir,), "w") as f:
+        f.write(conf)
+
+    cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
+    time.sleep(5)
+    cmd("%s/bin/createdb proxies" % (basedir,))
+    cmd("%s/bin/createdb augments" % (basedir,))
+    cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
+
+def doStart(basedir):
+    
+    cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
+
+def doStop(basedir):
+    
+    cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
+
+def doRun(basedir, verbose):
+    
+    cmd("%s/bin/postgres %s -D %s/data" % (basedir, "-d 3" if verbose else "",  basedir,))
+
+def doClean(basedir):
+    
+    cmd("rm -rf %s/data" % (basedir, basedir,))
+
+if __name__ == '__main__':
+
+    usage = "%prog [options] ACTION"
+    epilog = """
+ACTION is one of init|start|stop|run
+
+  init:   initialize databases
+  start:  start postgres daemon
+  stop:   stop postgres daemon
+  run:    run postgres (non-daemon)
+  clean:  remove databases
+  
+"""
+    description = "Tool to manage PostgreSQL"
+    version = "%prog v1.0"
+    parser = OptionParser(usage=usage, description=description, version=version)
+    parser.epilog = epilog
+    parser.format_epilog = lambda _:epilog
+
+    parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+                      default=True, help="Use debug logging for PostgreSQL")
+    parser.add_option("-d", "--base-dir", dest="basedir",
+                      default="%s/../postgresql-8.4.1/_root" % (os.getcwd(),), help="Base directory for PostgreSQL install")
+
+    (options, args) = parser.parse_args()
+
+    if len(args) != 1:
+        parser.error("incorrect number of arguments")
+
+    if args[0] == "init":
+        doInit(options.basedir)
+    elif args[0] == "start":
+        doStart(options.basedir)
+    elif args[0] == "stop":
+        doStop(options.basedir)
+    elif args[0] == "run":
+        doRun(options.basedir, options.verbose)
+    elif args[0] == "clean":
+        doClean(options.basedir)
+    else:
+        parser.error("incorrect argument '%s'" % (args[0],))

Modified: CalendarServer/trunk/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/principals.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/calendarserver/tools/principals.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -265,15 +265,10 @@
         #
         # Connect to memcached, notifications
         #
-        if config.Memcached.ClientEnabled:
-            memcachepool.installPool(
-                IPv4Address(
-                    "TCP",
-                    config.Memcached.BindAddress,
-                    config.Memcached.Port,
-                ),
-                config.Memcached.MaxClients
-            )
+        memcachepool.installPools(
+            config.Memcached.Pools,
+            config.Memcached.MaxClients
+        )
         if config.Notifications.Enabled:
             installNotificationClient(
                 config.Notifications.InternalNotificationHost,
@@ -479,11 +474,11 @@
             { True: "true", False: "false" }[autoSchedule],
             principal,
         )
-        (yield principal.setAutoSchedule(autoSchedule))
+        principal.setAutoSchedule(autoSchedule)
 
 @inlineCallbacks
 def action_getAutoSchedule(principal):
-    autoSchedule = (yield principal.getAutoSchedule())
+    autoSchedule = principal.getAutoSchedule()
     print "Autoschedule for %s is %s" % (
         principal,
         { True: "true", False: "false" }[autoSchedule],

Modified: CalendarServer/trunk/calendarserver/tools/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/util.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/calendarserver/tools/util.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2010 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.
@@ -29,6 +29,7 @@
 
 import socket
 from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.directory import augment, calendaruserproxy
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 
@@ -88,6 +89,13 @@
             return self.principalCollection.principalForCalendarUserAddress(cua)
 
 
+    # Load augment/proxy db classes now
+    augmentClass = namedClass(config.AugmentService.type)
+    augment.AugmentService = augmentClass(**config.AugmentService.params)
+
+    proxydbClass = namedClass(config.ProxyDBService.type)
+    calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
+
     # Wait for directory service to become available
     directory = MyDirectoryService(config.DirectoryService.params)
     while not directory.isAvailable():
@@ -128,14 +136,14 @@
     If memcached is not running, set config.Memcached.ClientEnabled to False
     """
 
-    if not config.Memcached.ClientEnabled:
+    if not config.Memcached.Pools.Default.ClientEnabled:
         return
 
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
     try:
-        s.connect((config.Memcached.BindAddress, config.Memcached.Port))
+        s.connect((config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port))
         s.close()
 
     except socket.error:
-        config.Memcached.ClientEnabled = False
+        config.Memcached.Pools.Default.ClientEnabled = False

Modified: CalendarServer/trunk/calendarserver/webadmin/resource.py
===================================================================
--- CalendarServer/trunk/calendarserver/webadmin/resource.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/calendarserver/webadmin/resource.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2010 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.
@@ -154,7 +154,7 @@
             # Update the auto-schedule value if specified.
             if autoSchedule is not None and (autoSchedule == "true" or autoSchedule == "false"):
                 if principal.record.recordType != "users" and principal.record.recordType != "groups":
-                    (yield principal.setAutoSchedule(autoSchedule == "true"))
+                    principal.setAutoSchedule(autoSchedule == "true")
 
             # Update the proxies if specified.
             for proxyId in removeProxies:
@@ -277,7 +277,7 @@
         ###
         autoScheduleHtml = ""
         if resource.record.recordType != "users" and resource.record.recordType != "groups":
-            autoSchedule = (yield resource.getAutoSchedule())
+            autoSchedule = resource.getAutoSchedule()
             autoScheduleHtml = """
 <div style=\"margin-top:15px; border-bottom:1px #444444 dotted\"></div>
 <form id=\"frm_autoSchedule\" name=\"autoScheduleForm\" action=\"/admin/\" style=\"margin-top:15px\">

Modified: CalendarServer/trunk/conf/auth/accounts-test.xml
===================================================================
--- CalendarServer/trunk/conf/auth/accounts-test.xml	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/conf/auth/accounts-test.xml	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+Copyright (c) 2006-2010 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.
@@ -52,27 +52,18 @@
     <name>Public %02d</name>
     <first-name>Public</first-name>
     <last-name>%02d</last-name>
-    <email-address>public%02d at example.com</email-address>
   </user>
   <location repeat="10">
     <uid>location%02d</uid>
     <guid>location%02d</guid>
     <password>location%02d</password>
     <name>Room %02d</name>
-    <auto-schedule/>
   </location>
   <resource repeat="10">
     <uid>resource%02d</uid>
     <guid>resource%02d</guid>
     <password>resource%02d</password>
     <name>Resource %02d</name>
-    <auto-schedule/>
-    <proxies>
-      <member type="users">user01</member>
-    </proxies>
-    <read-only-proxies>
-      <member type="users">user03</member>
-    </read-only-proxies>
   </resource>
   <group>
     <uid>group01</uid>
@@ -122,6 +113,5 @@
     <members>
       <member type="users">user01</member>
     </members>
-    <disable-calendar/>
   </group>
 </accounts>

Modified: CalendarServer/trunk/conf/auth/accounts.dtd
===================================================================
--- CalendarServer/trunk/conf/auth/accounts.dtd	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/conf/auth/accounts.dtd	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 <!--
-Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+Copyright (c) 2006-2010 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.
@@ -17,16 +17,16 @@
 <!ELEMENT accounts (user*, group*, resource*, location*) >
   <!ATTLIST accounts realm CDATA "">
 
-  <!ELEMENT user (uid+, guid, password, name, first-name?, last-name?, email-address*, disable-calendar?)>
+  <!ELEMENT user (uid*, guid, password?, name?, first-name?, last-name?, email-address*)>
     <!ATTLIST user repeat CDATA "1">
 
-  <!ELEMENT group (uid+, guid, password, name, members, disable-calendar?)>
+  <!ELEMENT group (uid*, guid, password?, name?, members)>
     <!ATTLIST group repeat CDATA "1">
 
-  <!ELEMENT resource (uid+, guid, password, name, auto-schedule?, proxies?, read-only-proxies?)>
+  <!ELEMENT resource (uid*, guid, password?, name?,)>
     <!ATTLIST resource repeat CDATA "1">
 
-  <!ELEMENT location (uid+, guid, password, name, auto-schedule?, proxies?, read-only-proxies?)>
+  <!ELEMENT location (uid*, guid, password?, name?,)>
     <!ATTLIST location repeat CDATA "1">
 
   <!ELEMENT member (#PCDATA)>
@@ -36,9 +36,8 @@
   <!ELEMENT guid              (#PCDATA)>
   <!ELEMENT password          (#PCDATA)>
   <!ELEMENT name              (#PCDATA)>
+  <!ELEMENT first-name        (#PCDATA)>
+  <!ELEMENT last-name         (#PCDATA)>
+  <!ELEMENT email-address     (#PCDATA)>
   <!ELEMENT members           (member*)>
-  <!ELEMENT auto-schedule     EMPTY>
-  <!ELEMENT disable-calendar  EMPTY>
-  <!ELEMENT proxies           (member*)>
-  <!ELEMENT read-only-proxies (member*)>
 >

Deleted: CalendarServer/trunk/conf/auth/accounts.htauth
===================================================================
--- CalendarServer/trunk/conf/auth/accounts.htauth	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/conf/auth/accounts.htauth	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,2 +0,0 @@
-admin:lzNnaQ7ZCPOlo
-test:m4cActsgkmlbI

Deleted: CalendarServer/trunk/conf/auth/accounts.htdigest
===================================================================
--- CalendarServer/trunk/conf/auth/accounts.htdigest	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/conf/auth/accounts.htdigest	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,3 +0,0 @@
-
-admin:Test Realm:ffa04aeb43a8a2efbe653956e04b739f
-test:Test Realm:688067d58e519c828459eee8c9f65043

Modified: CalendarServer/trunk/conf/auth/accounts.xml
===================================================================
--- CalendarServer/trunk/conf/auth/accounts.xml	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/conf/auth/accounts.xml	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+Copyright (c) 2006-2010 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.
@@ -28,7 +28,6 @@
     <uid>test</uid>
     <password>test</password>
     <name>Test User</name>
-    <email-address>testuser at example.com</email-address>
   </user>
   <group>
     <uid>users</uid>
@@ -42,10 +41,6 @@
     <uid>mercury</uid>
     <password>mercury</password>
     <name>Mecury Conference Room, Building 1, 2nd Floor</name>
-    <auto-schedule/>
-    <proxies>
-      <member type="users">test</member>
-    </proxies>
   </location>
 </accounts>
 

Copied: CalendarServer/trunk/conf/auth/augments-test.xml (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/conf/auth/augments-test.xml)
===================================================================
--- CalendarServer/trunk/conf/auth/augments-test.xml	                        (rev 0)
+++ CalendarServer/trunk/conf/auth/augments-test.xml	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2009-2010 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>
+  <record>
+    <guid>Default</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record repeat="10">
+    <guid>location%02d</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record repeat="10">
+    <guid>resource%02d</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record repeat="4">
+    <guid>group%02d</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>disabledgroup</guid>
+    <enable>false</enable>
+  </record>
+</augments>

Copied: CalendarServer/trunk/conf/auth/augments.dtd (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/conf/auth/augments.dtd)
===================================================================
--- CalendarServer/trunk/conf/auth/augments.dtd	                        (rev 0)
+++ CalendarServer/trunk/conf/auth/augments.dtd	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,27 @@
+<!--
+Copyright (c) 2009-2010 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.
+-->
+
+<!ELEMENT augments (record*) >
+
+  <!ELEMENT record (guid, enable, hosted-at?, enable-calendar?, auto-schedule?)>
+    <!ATTLIST record repeat CDATA "1">
+
+  <!ELEMENT guid              (#PCDATA)>
+  <!ELEMENT enable            (#PCDATA)>
+  <!ELEMENT hosted-at         (#PCDATA)>
+  <!ELEMENT enable-calendar   (#PCDATA)>
+  <!ELEMENT auto-schedule     (#PCDATA)>
+>

Copied: CalendarServer/trunk/conf/auth/proxies-test.xml (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/conf/auth/proxies-test.xml)
===================================================================
--- CalendarServer/trunk/conf/auth/proxies-test.xml	                        (rev 0)
+++ CalendarServer/trunk/conf/auth/proxies-test.xml	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2009-2010 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 proxies SYSTEM "proxies.dtd">
+
+<proxies>
+  <record repeat="10">
+    <guid>resource%02d</guid>
+    <proxies>
+      <member>user01</member>
+    </proxies>
+    <read-only-proxies>
+      <member>user03</member>
+    </read-only-proxies>
+  </record>
+</proxies>

Copied: CalendarServer/trunk/conf/auth/proxies.dtd (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/conf/auth/proxies.dtd)
===================================================================
--- CalendarServer/trunk/conf/auth/proxies.dtd	                        (rev 0)
+++ CalendarServer/trunk/conf/auth/proxies.dtd	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,26 @@
+<!--
+Copyright (c) 2009-2010 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.
+-->
+
+<!ELEMENT proxies (record*) >
+
+  <!ELEMENT record (guid, proxies?, read-only-proxies?)>
+    <!ATTLIST record repeat CDATA "1">
+
+  <!ELEMENT guid              (#PCDATA)>
+  <!ELEMENT proxies           (member*)>
+  <!ELEMENT read-only-proxies (member*)>
+  <!ELEMENT member            (#PCDATA)>
+>

Modified: CalendarServer/trunk/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-apple.plist	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/conf/caldavd-apple.plist	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!--
-    Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+    Copyright (c) 2006-2010 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.
@@ -160,6 +160,92 @@
 
 
     <!--
+        Augment service
+
+        Augments for the directory service records to add calendar specific attributes.
+
+        A variety of augment services are available for use.
+        When using a partitioned server, a service that can be accessed from each host will be needed.
+      -->
+
+    <!-- XML File Augment Service -->
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>xmlFiles</key>
+        <array>
+	      <string>/etc/caldavd/augments.xml</string>
+        </array>
+      </dict>
+    </dict>
+    
+    <!-- Sqlite Augment Service -->
+    <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentSqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>/etc/caldavd/augments.sqlite</string>
+      </dict>
+    </dict>
+     -->
+
+    <!-- PostgreSQL Augment Service -->
+    <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>augments</string>
+      </dict>
+    </dict>
+     -->
+
+    <!-- Sqlite ProxyDB Service -->
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>/etc/caldavd/proxies.sqlite</string>
+      </dict>
+    </dict>
+
+    <!-- PostgreSQL ProxyDB Service -->
+    <!--
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>proxies</string>
+      </dict>
+    </dict>
+     -->
+
+    <!--
         Special principals
 
         These principals are granted special access and/or perform

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!--
-    Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+    Copyright (c) 2006-2010 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.
@@ -145,71 +145,100 @@
       <dict>
         <key>node</key>
         <string>/Search</string>
-        <key>restrictEnabledRecords</key>
-        <false/>
-        <key>restrictToGroup</key>
-        <string></string>
         <key>cacheTimeout</key>
         <integer>30</integer>
       </dict>
     </dict>
     -->
 
-    <!-- Apache-style Basic Directory Service (Experimental) -->
     <!--
-    <key>DirectoryService</key>
+        Augment service
+
+        Augments for the directory service records to add calendar specific attributes.
+
+        A variety of augment services are available for use.
+        When using a partitioned server, a service that can be accessed from each host will be needed.
+      -->
+
+    <!-- XML File Augment Service -->
+    <key>AugmentService</key>
     <dict>
       <key>type</key>
-      <string>twistedcaldav.directory.apache.BasicDirectoryService</string>
+      <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
       
       <key>params</key>
       <dict>
-        <key>realmName</key>
-        <string>Test Realm</string>
-        <key>userFile</key>
-        <string>conf/auth/accounts.htauth</string>
-        <key>groupFile</key>
-        <string>conf/auth/groups</string>
+        <key>xmlFiles</key>
+        <array>
+	      <string>conf/auth/augments-test.xml</string>
+        </array>
       </dict>
     </dict>
-    -->
+    
+    <!-- Sqlite Augment Service -->
+    <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentSqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>/etc/caldavd/augments.sqlite</string>
+      </dict>
+    </dict>
+     -->
 
-    <!-- Apache-style Digest Directory Service (Experimental) -->
+    <!-- PostgreSQL Augment Service -->
     <!--
-    <key>DirectoryService</key>
+    <key>AugmentService</key>
     <dict>
       <key>type</key>
-      <string>twistedcaldav.directory.apache.DigestDirectoryService</string>
+      <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
       
       <key>params</key>
       <dict>
-        <key>realmName</key>
-        <string>Test Realm</string>
-        <key>userFile</key>
-        <string>conf/auth/accounts.htdigest</string>
-        <key>groupFile</key>
-        <string>conf/auth/groups</string>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>augments</string>
       </dict>
     </dict>
      -->
 
-    <!-- SQL Directory Service (Experimental) -->
+    <!-- Sqlite ProxyDB Service -->
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>data/proxies.sqlite</string>
+      </dict>
+    </dict>
+
+    <!-- PostgreSQL ProxyDB Service -->
     <!--
-    <key>DirectoryService</key>
+    <key>ProxyDBService</key>
     <dict>
       <key>type</key>
-      <string>twistedcaldav.directory.sqldb.SQLDirectoryService</string>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
       
       <key>params</key>
       <dict>
-        <key>dbParentPath</key>
-        <string>twistedcaldav/test/data/</string>
-        <key>xmlFile</key>
-        <string>conf/auth/accounts-test.xml</string>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>proxies</string>
       </dict>
     </dict>
-    -->
+     -->
 
+	<key>ProxyLoadFromFile</key>
+    <string>conf/auth/proxies-test.xml</string>
 
     <!--
         Special principals
@@ -655,10 +684,6 @@
     <!-- Support for Memcached -->
     <key>Memcached</key>
     <dict>
-      <key>ServerEnabled</key>
-      <true/>
-      <key>ClientEnabled</key>
-      <true/>
       <key>MaxClients</key>
       <integer>5</integer>
       <key>memcached</key>

Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/conf/caldavd.plist	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!--
-    Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+    Copyright (c) 2006-2010 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.
@@ -148,10 +148,6 @@
       <dict>
         <key>node</key>
         <string>/Search</string>
-        <key>restrictEnabledRecords</key>
-        <false/>
-        <key>restrictToGroup</key>
-        <string></string>
         <key>cacheTimeout</key>
         <integer>30</integer>
       </dict>
@@ -159,6 +155,92 @@
      -->
 
     <!--
+        Augment service
+
+        Augments for the directory service records to add calendar specific attributes.
+
+        A variety of augment services are available for use.
+        When using a partitioned server, a service that can be accessed from each host will be needed.
+      -->
+
+    <!-- XML File Augment Service -->
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>xmlFiles</key>
+        <array>
+	      <string>/etc/caldavd/augments.xml</string>
+        </array>
+      </dict>
+    </dict>
+    
+    <!-- Sqlite Augment Service -->
+    <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentSqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>/etc/caldavd/augments.sqlite</string>
+      </dict>
+    </dict>
+     -->
+
+    <!-- PostgreSQL Augment Service -->
+    <!--
+    <key>AugmentService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>augments</string>
+      </dict>
+    </dict>
+     -->
+
+    <!-- Sqlite ProxyDB Service -->
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>dbpath</key>
+        <string>/etc/caldavd/proxies.sqlite</string>
+      </dict>
+    </dict>
+
+    <!-- PostgreSQL ProxyDB Service -->
+    <!--
+    <key>ProxyDBService</key>
+    <dict>
+      <key>type</key>
+      <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
+      
+      <key>params</key>
+      <dict>
+        <key>host</key>
+        <string>localhost</string>
+        <key>database</key>
+        <string>proxies</string>
+      </dict>
+    </dict>
+     -->
+
+    <!--
         Special principals
 
         These principals are granted special access and/or perform

Copied: CalendarServer/trunk/conf/partitions-test.plist (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/conf/partitions-test.plist)
===================================================================
--- CalendarServer/trunk/conf/partitions-test.plist	                        (rev 0)
+++ CalendarServer/trunk/conf/partitions-test.plist	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2009-2010 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 plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>partitions</key>
+  <array>
+    <dict>
+      <key>uid</key>
+      <string>00001</string>
+      <key>url</key>
+      <string>http://localhost:8008</string>
+    </dict>
+    <dict>
+      <key>uid</key>
+      <string>00002</string>
+      <key>url</key>
+      <string>http://localhost:8108</string>
+    </dict>
+  </array>
+</dict>
+</plist>

Copied: CalendarServer/trunk/conf/partitions.plist (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/conf/partitions.plist)
===================================================================
--- CalendarServer/trunk/conf/partitions.plist	                        (rev 0)
+++ CalendarServer/trunk/conf/partitions.plist	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+    Copyright (c) 2009-2010 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 plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>partitions</key>
+  <array>
+  <!--
+    <dict>
+      <key>uid</key>
+      <string>00001</string>
+      <key>url</key>
+      <string>http://localhost:8008</string>
+    </dict>
+  -->
+  </array>
+</dict>
+</plist>

Modified: CalendarServer/trunk/contrib/migration/59_calendarmigrator.py
===================================================================
--- CalendarServer/trunk/contrib/migration/59_calendarmigrator.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/contrib/migration/59_calendarmigrator.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -10,7 +10,7 @@
 # The only argument this script currently cares about is --sourceRoot, which
 # should point to the root of the previous system.
 #
-# Copyright (c) 2005-2009 Apple Inc.  All Rights Reserved.
+# Copyright (c) 2005-2010 Apple Inc.  All Rights Reserved.
 #
 # IMPORTANT NOTE:  This file is licensed only for use on Apple-labeled
 # computers and is subject to the terms and conditions of the Apple
@@ -187,7 +187,7 @@
         newPlist["DirectoryService"] = oldPlist["DirectoryService"]
         for key in newPlist["DirectoryService"]["params"].keys():
             if key not in (
-                "node", "restrictEnabledRecords", "restrictToGroup",
+                "node",
                 "cacheTimeout", "xmlFile"
             ):
                 del newPlist["DirectoryService"]["params"][key]


Property changes on: CalendarServer/trunk/doc/Extensions/caldav-privatecomments.txt
___________________________________________________________________
Added: svn:executable
   + *


Property changes on: CalendarServer/trunk/doc/Extensions/caldav-privatecomments.xml
___________________________________________________________________
Added: svn:executable
   + *

Modified: CalendarServer/trunk/memcacheclient.py
===================================================================
--- CalendarServer/trunk/memcacheclient.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/memcacheclient.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -124,7 +124,7 @@
                  pickler=pickle.Pickler, unpickler=pickle.Unpickler,
                  pload=None, pid=None):
 
-        if config.Memcached.ClientEnabled:
+        if config.Memcached.Pools.Default.ClientEnabled:
             return Client(servers, debug=debug, pickleProtocol=pickleProtocol,
                 pickler=pickler, unpickler=unpickler, pload=pload, pid=pid)
         elif cls.allowTestCache:

Modified: CalendarServer/trunk/support/build.sh
===================================================================
--- CalendarServer/trunk/support/build.sh	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/support/build.sh	2010-01-25 15:56:33 UTC (rev 4958)
@@ -466,6 +466,10 @@
       true false false false 0;
   fi;
 
+  py_dependency "PyGreSQL" "pgdb" "PyGreSQL-4.0" \
+    "www" "ftp://ftp.pygresql.org/pub/distrib/PyGreSQL.tgz" \
+    false false false false 0;
+
   py_dependency "Twisted" "twisted" "Twisted" \
     "svn" "svn://svn.twistedmatrix.com/svn/Twisted/branches/dav-take-two-3081-4" \
     false true true false 27622;

Modified: CalendarServer/trunk/twistedcaldav/cache.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/cache.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/cache.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2010 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.
@@ -28,7 +28,7 @@
 from twisted.web2.stream import MemoryStream
 
 from twistedcaldav.log import LoggingMixIn
-from twistedcaldav.memcachepool import CachePoolUserMixIn
+from twistedcaldav.memcachepool import CachePoolUserMixIn, defaultCachePool
 from twistedcaldav.config import config
 
 
@@ -61,9 +61,10 @@
 
 class MemcacheChangeNotifier(LoggingMixIn, CachePoolUserMixIn):
 
-    def __init__(self, resource, cachePool=None):
+    def __init__(self, resource, cachePool=None, cacheHandle="Default"):
         self._resource = resource
         self._cachePool = cachePool
+        self._cachePoolHandle = cacheHandle
 
     def _newCacheToken(self):
         return str(uuid.uuid4())
@@ -162,7 +163,7 @@
         self._cachePool = cachePool
 
 
-    def _tokenForURI(self, uri):
+    def _tokenForURI(self, uri, cachePoolHandle=None):
         """
         Get a property store for the given C{uri}.
 
@@ -170,13 +171,16 @@
         @return: A C{str} representing the token for the URI.
         """
 
-        return self.getCachePool().get('cacheToken:%s' % (uri,))
+        if cachePoolHandle:
+            return defaultCachePool(cachePoolHandle).get('cacheToken:%s' % (uri,))
+        else:
+            return self.getCachePool().get('cacheToken:%s' % (uri,))
 
 
     def _getTokens(self, request):
         def _tokensForURIs((pURI, rURI)):
             tokens = []
-            d1 = self._tokenForURI(pURI)
+            d1 = self._tokenForURI(pURI, "PrincipalToken")
             d1.addCallback(tokens.append)
             d1.addCallback(lambda _ign: self._getRecordForURI(pURI, request))
             d1.addCallback(lambda dToken: tokens.append(hash(dToken)))

Deleted: CalendarServer/trunk/twistedcaldav/client/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/client/__init__.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/client/__init__.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,16 +0,0 @@
-##
-# Copyright (c) 2009 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.
-##
-

Copied: CalendarServer/trunk/twistedcaldav/client/__init__.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/client/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/client/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/client/__init__.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+

Deleted: CalendarServer/trunk/twistedcaldav/client/pool.py
===================================================================
--- CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/client/pool.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/client/pool.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,391 +0,0 @@
-##
-# Copyright (c) 2009 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.
-##
-
-__all__ = [
-    "installPools",
-    "installPool",
-    "getHTTPClientPool",
-]
-
-from twext.internet.ssl import ChainingOpenSSLContextFactory
-from twisted.internet.address import IPv4Address
-from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
-from twisted.internet.error import ConnectionLost, ConnectionDone, ConnectError
-from twisted.internet.protocol import ClientFactory
-from twisted.web2 import responsecode
-from twisted.web2.client.http import HTTPClientProtocol
-from twisted.web2.http import StatusResponse, HTTPError
-from twistedcaldav.config import config
-from twistedcaldav.log import LoggingMixIn
-import OpenSSL
-import urlparse
-
-class PooledHTTPClientFactory(ClientFactory, LoggingMixIn):
-    """
-    A client factory for HTTPClient that notifies a pool of it's state. It the connection
-    fails in the middle of a request it will retry the request.
-
-    @ivar protocol: The current instance of our protocol that we pass
-        to our connectionPool.
-    @ivar connectionPool: A managing connection pool that we notify of events.
-    """
-    protocol = HTTPClientProtocol
-    connectionPool = None
-
-    def __init__(self, reactor):
-        self.reactor = reactor
-        self.instance = None
-        self.onConnect = Deferred()
-        self.afterConnect = Deferred()
-
-    def clientConnectionLost(self, connector, reason):
-        """
-        Notify the connectionPool that we've lost our connection.
-        """
-
-        if hasattr(self, "afterConnect"):
-            self.reactor.callLater(0, self.afterConnect.errback, reason)
-            del self.afterConnect
-
-        if self.connectionPool.shutdown_requested:
-            # The reactor is stopping; don't reconnect
-            return
-
-    def clientConnectionFailed(self, connector, reason):
-        """
-        Notify the connectionPool that we're unable to connect
-        """
-        if hasattr(self, "onConnect"):
-            self.reactor.callLater(0, self.onConnect.errback, reason)
-            del self.onConnect
-        elif hasattr(self, "afterConnect"):
-            self.reactor.callLater(0, self.afterConnect.errback, reason)
-            del self.afterConnect
-
-    def buildProtocol(self, addr):
-        self.instance = self.protocol()
-        self.reactor.callLater(0, self.onConnect.callback, self.instance)
-        del self.onConnect
-        return self.instance
-
-class HTTPClientPool(LoggingMixIn):
-    """
-    A connection pool for HTTPClientProtocol instances.
-
-    @ivar clientFactory: The L{ClientFactory} implementation that will be used
-        for each protocol.
-
-    @ivar _maxClients: A C{int} indicating the maximum number of clients.
-    @ivar _serverAddress: An L{IAddress} provider indicating the server to
-        connect to.  (Only L{IPv4Address} currently supported.)
-    @ivar _reactor: The L{IReactorTCP} provider used to initiate new
-        connections.
-
-    @ivar _busyClients: A C{set} that contains all currently busy clients.
-    @ivar _freeClients: A C{set} that contains all currently free clients.
-    @ivar _pendingConnects: A C{int} indicating how many connections are in
-        progress.
-    """
-    clientFactory = PooledHTTPClientFactory
-    maxRetries = 2
-
-    def __init__(self, name, scheme, serverAddress, maxClients=5, reactor=None):
-        """
-        @param serverAddress: An L{IPv4Address} indicating the server to
-            connect to.
-        @param maxClients: A C{int} indicating the maximum number of clients.
-        @param reactor: An L{IReactorTCP{ provider used to initiate new
-            connections.
-        """
-        
-        self._name = name
-        self._scheme = scheme
-        self._serverAddress = serverAddress
-        self._maxClients = maxClients
-
-        if reactor is None:
-            from twisted.internet import reactor
-        self._reactor = reactor
-
-        self.shutdown_deferred = None
-        self.shutdown_requested = False
-        reactor.addSystemEventTrigger('before', 'shutdown', self._shutdownCallback)
-
-        self._busyClients = set([])
-        self._freeClients = set([])
-        self._pendingConnects = 0
-        self._pendingRequests = []
-
-    def _isIdle(self):
-        return (
-            len(self._busyClients) == 0 and
-            len(self._pendingRequests) == 0 and
-            self._pendingConnects == 0
-        )
-
-    def _shutdownCallback(self):
-        self.shutdown_requested = True
-        if self._isIdle():
-            return None
-        self.shutdown_deferred = Deferred()
-        return self.shutdown_deferred
-
-    def _newClientConnection(self):
-        """
-        Create a new client connection.
-
-        @return: A L{Deferred} that fires with the L{IProtocol} instance.
-        """
-        self.log_debug("Initiating new client connection to: %s" % (self._serverAddress,))
-        self._logClientStats()
-
-        self._pendingConnects += 1
-
-        factory = self.clientFactory(self._reactor)
-        factory.connectionPool = self
-
-        if self._scheme == "https":
-            context = ChainingOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, certificateChainFile=config.SSLAuthorityChain, sslmethod=getattr(OpenSSL.SSL, config.SSLMethod))
-            self._reactor.connectSSL(self._serverAddress.host, self._serverAddress.port, factory, context)
-        elif self._scheme == "http":
-            self._reactor.connectTCP(self._serverAddress.host, self._serverAddress.port, factory)
-        else:
-            raise ValueError("URL scheme for client pool not supported")
-
-        def _doneOK(client):
-            self._pendingConnects -= 1
-
-            def _goneClientAfterError(f, client):
-                f.trap(ConnectionLost, ConnectionDone, ConnectError)
-                self.clientGone(client)
-
-            d2 = factory.afterConnect
-            d2.addErrback(_goneClientAfterError, client)
-            return client
-
-        def _doneError(result):
-            self._pendingConnects -= 1
-            return result
-
-        d = factory.onConnect
-        d.addCallbacks(_doneOK, _doneError)
-        
-        return d
-
-    def _performRequestOnClient(self, client, request, *args, **kwargs):
-        """
-        Perform the given request on the given client.
-
-        @param client: A L{PooledMemCacheProtocol} that will be used to perform
-            the given request.
-
-        @param command: A C{str} representing an attribute of
-            L{MemCacheProtocol}.
-        @parma args: Any positional arguments that should be passed to
-            C{command}.
-        @param kwargs: Any keyword arguments that should be passed to
-            C{command}.
-
-        @return: A L{Deferred} that fires with the result of the given command.
-        """
-
-        def _freeClientAfterRequest(result):
-            self.clientFree(client)
-            return result
-
-        def _goneClientAfterError(result):
-            self.clientGone(client)
-            return result
-
-        self.clientBusy(client)
-        d = client.submitRequest(request, closeAfter=False)
-        d.addCallbacks(_freeClientAfterRequest, _goneClientAfterError)
-        return d
-
-    @inlineCallbacks
-    def submitRequest(self, request, *args, **kwargs):
-        """
-        Select an available client and perform the given request on it.
-
-        @param command: A C{str} representing an attribute of
-            L{MemCacheProtocol}.
-        @parma args: Any positional arguments that should be passed to
-            C{command}.
-        @param kwargs: Any keyword arguments that should be passed to
-            C{command}.
-
-        @return: A L{Deferred} that fires with the result of the given command.
-        """
-
-        # Try this maxRetries times
-        for ctr in xrange(self.maxRetries + 1):
-            try:
-                response = (yield self._submitRequest(request, args, kwargs))
-
-            except (ConnectionLost, ConnectionDone, ConnectError), e:
-                self.log_error("HTTP pooled client connection error (attempt: %d) - retrying: %s" % (ctr+1, e,))
-                continue
-            
-            # TODO: find the proper cause of these assertions and fix
-            except (AssertionError,), e:
-                self.log_error("HTTP pooled client connection assertion error (attempt: %d) - retrying: %s" % (ctr+1, e,))
-                continue
-
-            else:
-                returnValue(response)
-        else:
-            self.log_error("HTTP pooled client connection error - exhausted retry attempts.")
-            raise HTTPError(StatusResponse(responsecode.BAD_GATEWAY, "Could not connect to HTTP pooled client host."))
-
-    def _submitRequest(self, request, *args, **kwargs):
-        """
-        Select an available client and perform the given request on it.
-
-        @param command: A C{str} representing an attribute of
-            L{MemCacheProtocol}.
-        @parma args: Any positional arguments that should be passed to
-            C{command}.
-        @param kwargs: Any keyword arguments that should be passed to
-            C{command}.
-
-        @return: A L{Deferred} that fires with the result of the given command.
-        """
-
-        if len(self._freeClients) > 0:
-            d = self._performRequestOnClient(self._freeClients.pop(), request, *args, **kwargs)
-
-        elif len(self._busyClients) + self._pendingConnects >= self._maxClients:
-            d = Deferred()
-            self._pendingRequests.append((d, request, args, kwargs))
-            self.log_debug("Request queued: %s, %r, %r" % (request, args, kwargs))
-            self._logClientStats()
-
-        else:
-            d = self._newClientConnection()
-            d.addCallback(self._performRequestOnClient, request, *args, **kwargs)
-
-        return d
-
-    def _logClientStats(self):
-        self.log_debug("Clients #free: %d, #busy: %d, "
-                       "#pending: %d, #queued: %d" % (
-                len(self._freeClients),
-                len(self._busyClients),
-                self._pendingConnects,
-                len(self._pendingRequests)))
-
-    def clientGone(self, client):
-        """
-        Notify that the given client is to be removed from the pool completely.
-
-        @param client: An instance of L{PooledMemCacheProtocol}.
-        """
-        if client in self._busyClients:
-            self._busyClients.remove(client)
-
-        elif client in self._freeClients:
-            self._freeClients.remove(client)
-
-        self.log_debug("Removed client: %r" % (client,))
-        self._logClientStats()
-
-        self._processPending()
-
-    def clientBusy(self, client):
-        """
-        Notify that the given client is being used to complete a request.
-
-        @param client: An instance of C{self.clientFactory}
-        """
-
-        if client in self._freeClients:
-            self._freeClients.remove(client)
-
-        self._busyClients.add(client)
-
-        self.log_debug("Busied client: %r" % (client,))
-        self._logClientStats()
-
-    def clientFree(self, client):
-        """
-        Notify that the given client is free to handle more requests.
-
-        @param client: An instance of C{self.clientFactory}
-        """
-        if client in self._busyClients:
-            self._busyClients.remove(client)
-
-        self._freeClients.add(client)
-
-        if self.shutdown_deferred and self._isIdle():
-            self.shutdown_deferred.callback(None)
-
-        self.log_debug("Freed client: %r" % (client,))
-        self._logClientStats()
-
-        self._processPending()
-
-    def _processPending(self):
-        if len(self._pendingRequests) > 0:
-            d, request, args, kwargs = self._pendingRequests.pop(0)
-
-            self.log_debug("Performing Queued Request: %s, %r, %r" % (
-                    request, args, kwargs))
-            self._logClientStats()
-
-            _ign_d = self._submitRequest(request, *args, **kwargs)
-
-            _ign_d.addCallbacks(d.callback, d.errback)
-
-    def suggestMaxClients(self, maxClients):
-        """
-        Suggest the maximum number of concurrently connected clients.
-
-        @param maxClients: A C{int} indicating how many client connections we
-            should keep open.
-        """
-        self._maxClients = maxClients
-
-_clientPools = {}     # Maps a host:port to a pool object
-
-def installPools(hosts, maxClients=5, reactor=None):
-    
-    for name, url in hosts:
-        installPool(
-            name,
-            url,
-            maxClients,
-            reactor,
-        )
-
-def installPool(name, url, maxClients=5, reactor=None):
-
-    parsedURL = urlparse.urlparse(url)
-    pool = HTTPClientPool(
-        name,
-        parsedURL.scheme,
-        IPv4Address(
-            "TCP",
-            parsedURL.hostname,
-            parsedURL.port,
-        ),
-        maxClients,
-        reactor,
-    )
-    _clientPools[name] = pool
-
-def getHTTPClientPool(name):
-    return _clientPools[name]

Copied: CalendarServer/trunk/twistedcaldav/client/pool.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/client/pool.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/client/pool.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/client/pool.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,391 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+__all__ = [
+    "installPools",
+    "installPool",
+    "getHTTPClientPool",
+]
+
+from twext.internet.ssl import ChainingOpenSSLContextFactory
+from twisted.internet.address import IPv4Address
+from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
+from twisted.internet.error import ConnectionLost, ConnectionDone, ConnectError
+from twisted.internet.protocol import ClientFactory
+from twisted.web2 import responsecode
+from twisted.web2.client.http import HTTPClientProtocol
+from twisted.web2.http import StatusResponse, HTTPError
+from twistedcaldav.config import config
+from twistedcaldav.log import LoggingMixIn
+import OpenSSL
+import urlparse
+
+class PooledHTTPClientFactory(ClientFactory, LoggingMixIn):
+    """
+    A client factory for HTTPClient that notifies a pool of it's state. It the connection
+    fails in the middle of a request it will retry the request.
+
+    @ivar protocol: The current instance of our protocol that we pass
+        to our connectionPool.
+    @ivar connectionPool: A managing connection pool that we notify of events.
+    """
+    protocol = HTTPClientProtocol
+    connectionPool = None
+
+    def __init__(self, reactor):
+        self.reactor = reactor
+        self.instance = None
+        self.onConnect = Deferred()
+        self.afterConnect = Deferred()
+
+    def clientConnectionLost(self, connector, reason):
+        """
+        Notify the connectionPool that we've lost our connection.
+        """
+
+        if hasattr(self, "afterConnect"):
+            self.reactor.callLater(0, self.afterConnect.errback, reason)
+            del self.afterConnect
+
+        if self.connectionPool.shutdown_requested:
+            # The reactor is stopping; don't reconnect
+            return
+
+    def clientConnectionFailed(self, connector, reason):
+        """
+        Notify the connectionPool that we're unable to connect
+        """
+        if hasattr(self, "onConnect"):
+            self.reactor.callLater(0, self.onConnect.errback, reason)
+            del self.onConnect
+        elif hasattr(self, "afterConnect"):
+            self.reactor.callLater(0, self.afterConnect.errback, reason)
+            del self.afterConnect
+
+    def buildProtocol(self, addr):
+        self.instance = self.protocol()
+        self.reactor.callLater(0, self.onConnect.callback, self.instance)
+        del self.onConnect
+        return self.instance
+
+class HTTPClientPool(LoggingMixIn):
+    """
+    A connection pool for HTTPClientProtocol instances.
+
+    @ivar clientFactory: The L{ClientFactory} implementation that will be used
+        for each protocol.
+
+    @ivar _maxClients: A C{int} indicating the maximum number of clients.
+    @ivar _serverAddress: An L{IAddress} provider indicating the server to
+        connect to.  (Only L{IPv4Address} currently supported.)
+    @ivar _reactor: The L{IReactorTCP} provider used to initiate new
+        connections.
+
+    @ivar _busyClients: A C{set} that contains all currently busy clients.
+    @ivar _freeClients: A C{set} that contains all currently free clients.
+    @ivar _pendingConnects: A C{int} indicating how many connections are in
+        progress.
+    """
+    clientFactory = PooledHTTPClientFactory
+    maxRetries = 2
+
+    def __init__(self, name, scheme, serverAddress, maxClients=5, reactor=None):
+        """
+        @param serverAddress: An L{IPv4Address} indicating the server to
+            connect to.
+        @param maxClients: A C{int} indicating the maximum number of clients.
+        @param reactor: An L{IReactorTCP{ provider used to initiate new
+            connections.
+        """
+        
+        self._name = name
+        self._scheme = scheme
+        self._serverAddress = serverAddress
+        self._maxClients = maxClients
+
+        if reactor is None:
+            from twisted.internet import reactor
+        self._reactor = reactor
+
+        self.shutdown_deferred = None
+        self.shutdown_requested = False
+        reactor.addSystemEventTrigger('before', 'shutdown', self._shutdownCallback)
+
+        self._busyClients = set([])
+        self._freeClients = set([])
+        self._pendingConnects = 0
+        self._pendingRequests = []
+
+    def _isIdle(self):
+        return (
+            len(self._busyClients) == 0 and
+            len(self._pendingRequests) == 0 and
+            self._pendingConnects == 0
+        )
+
+    def _shutdownCallback(self):
+        self.shutdown_requested = True
+        if self._isIdle():
+            return None
+        self.shutdown_deferred = Deferred()
+        return self.shutdown_deferred
+
+    def _newClientConnection(self):
+        """
+        Create a new client connection.
+
+        @return: A L{Deferred} that fires with the L{IProtocol} instance.
+        """
+        self.log_debug("Initiating new client connection to: %s" % (self._serverAddress,))
+        self._logClientStats()
+
+        self._pendingConnects += 1
+
+        factory = self.clientFactory(self._reactor)
+        factory.connectionPool = self
+
+        if self._scheme == "https":
+            context = ChainingOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, certificateChainFile=config.SSLAuthorityChain, sslmethod=getattr(OpenSSL.SSL, config.SSLMethod))
+            self._reactor.connectSSL(self._serverAddress.host, self._serverAddress.port, factory, context)
+        elif self._scheme == "http":
+            self._reactor.connectTCP(self._serverAddress.host, self._serverAddress.port, factory)
+        else:
+            raise ValueError("URL scheme for client pool not supported")
+
+        def _doneOK(client):
+            self._pendingConnects -= 1
+
+            def _goneClientAfterError(f, client):
+                f.trap(ConnectionLost, ConnectionDone, ConnectError)
+                self.clientGone(client)
+
+            d2 = factory.afterConnect
+            d2.addErrback(_goneClientAfterError, client)
+            return client
+
+        def _doneError(result):
+            self._pendingConnects -= 1
+            return result
+
+        d = factory.onConnect
+        d.addCallbacks(_doneOK, _doneError)
+        
+        return d
+
+    def _performRequestOnClient(self, client, request, *args, **kwargs):
+        """
+        Perform the given request on the given client.
+
+        @param client: A L{PooledMemCacheProtocol} that will be used to perform
+            the given request.
+
+        @param command: A C{str} representing an attribute of
+            L{MemCacheProtocol}.
+        @parma args: Any positional arguments that should be passed to
+            C{command}.
+        @param kwargs: Any keyword arguments that should be passed to
+            C{command}.
+
+        @return: A L{Deferred} that fires with the result of the given command.
+        """
+
+        def _freeClientAfterRequest(result):
+            self.clientFree(client)
+            return result
+
+        def _goneClientAfterError(result):
+            self.clientGone(client)
+            return result
+
+        self.clientBusy(client)
+        d = client.submitRequest(request, closeAfter=False)
+        d.addCallbacks(_freeClientAfterRequest, _goneClientAfterError)
+        return d
+
+    @inlineCallbacks
+    def submitRequest(self, request, *args, **kwargs):
+        """
+        Select an available client and perform the given request on it.
+
+        @param command: A C{str} representing an attribute of
+            L{MemCacheProtocol}.
+        @parma args: Any positional arguments that should be passed to
+            C{command}.
+        @param kwargs: Any keyword arguments that should be passed to
+            C{command}.
+
+        @return: A L{Deferred} that fires with the result of the given command.
+        """
+
+        # Try this maxRetries times
+        for ctr in xrange(self.maxRetries + 1):
+            try:
+                response = (yield self._submitRequest(request, args, kwargs))
+
+            except (ConnectionLost, ConnectionDone, ConnectError), e:
+                self.log_error("HTTP pooled client connection error (attempt: %d) - retrying: %s" % (ctr+1, e,))
+                continue
+            
+            # TODO: find the proper cause of these assertions and fix
+            except (AssertionError,), e:
+                self.log_error("HTTP pooled client connection assertion error (attempt: %d) - retrying: %s" % (ctr+1, e,))
+                continue
+
+            else:
+                returnValue(response)
+        else:
+            self.log_error("HTTP pooled client connection error - exhausted retry attempts.")
+            raise HTTPError(StatusResponse(responsecode.BAD_GATEWAY, "Could not connect to HTTP pooled client host."))
+
+    def _submitRequest(self, request, *args, **kwargs):
+        """
+        Select an available client and perform the given request on it.
+
+        @param command: A C{str} representing an attribute of
+            L{MemCacheProtocol}.
+        @parma args: Any positional arguments that should be passed to
+            C{command}.
+        @param kwargs: Any keyword arguments that should be passed to
+            C{command}.
+
+        @return: A L{Deferred} that fires with the result of the given command.
+        """
+
+        if len(self._freeClients) > 0:
+            d = self._performRequestOnClient(self._freeClients.pop(), request, *args, **kwargs)
+
+        elif len(self._busyClients) + self._pendingConnects >= self._maxClients:
+            d = Deferred()
+            self._pendingRequests.append((d, request, args, kwargs))
+            self.log_debug("Request queued: %s, %r, %r" % (request, args, kwargs))
+            self._logClientStats()
+
+        else:
+            d = self._newClientConnection()
+            d.addCallback(self._performRequestOnClient, request, *args, **kwargs)
+
+        return d
+
+    def _logClientStats(self):
+        self.log_debug("Clients #free: %d, #busy: %d, "
+                       "#pending: %d, #queued: %d" % (
+                len(self._freeClients),
+                len(self._busyClients),
+                self._pendingConnects,
+                len(self._pendingRequests)))
+
+    def clientGone(self, client):
+        """
+        Notify that the given client is to be removed from the pool completely.
+
+        @param client: An instance of L{PooledMemCacheProtocol}.
+        """
+        if client in self._busyClients:
+            self._busyClients.remove(client)
+
+        elif client in self._freeClients:
+            self._freeClients.remove(client)
+
+        self.log_debug("Removed client: %r" % (client,))
+        self._logClientStats()
+
+        self._processPending()
+
+    def clientBusy(self, client):
+        """
+        Notify that the given client is being used to complete a request.
+
+        @param client: An instance of C{self.clientFactory}
+        """
+
+        if client in self._freeClients:
+            self._freeClients.remove(client)
+
+        self._busyClients.add(client)
+
+        self.log_debug("Busied client: %r" % (client,))
+        self._logClientStats()
+
+    def clientFree(self, client):
+        """
+        Notify that the given client is free to handle more requests.
+
+        @param client: An instance of C{self.clientFactory}
+        """
+        if client in self._busyClients:
+            self._busyClients.remove(client)
+
+        self._freeClients.add(client)
+
+        if self.shutdown_deferred and self._isIdle():
+            self.shutdown_deferred.callback(None)
+
+        self.log_debug("Freed client: %r" % (client,))
+        self._logClientStats()
+
+        self._processPending()
+
+    def _processPending(self):
+        if len(self._pendingRequests) > 0:
+            d, request, args, kwargs = self._pendingRequests.pop(0)
+
+            self.log_debug("Performing Queued Request: %s, %r, %r" % (
+                    request, args, kwargs))
+            self._logClientStats()
+
+            _ign_d = self._submitRequest(request, *args, **kwargs)
+
+            _ign_d.addCallbacks(d.callback, d.errback)
+
+    def suggestMaxClients(self, maxClients):
+        """
+        Suggest the maximum number of concurrently connected clients.
+
+        @param maxClients: A C{int} indicating how many client connections we
+            should keep open.
+        """
+        self._maxClients = maxClients
+
+_clientPools = {}     # Maps a host:port to a pool object
+
+def installPools(hosts, maxClients=5, reactor=None):
+    
+    for name, url in hosts:
+        installPool(
+            name,
+            url,
+            maxClients,
+            reactor,
+        )
+
+def installPool(name, url, maxClients=5, reactor=None):
+
+    parsedURL = urlparse.urlparse(url)
+    pool = HTTPClientPool(
+        name,
+        parsedURL.scheme,
+        IPv4Address(
+            "TCP",
+            parsedURL.hostname,
+            parsedURL.port,
+        ),
+        maxClients,
+        reactor,
+    )
+    _clientPools[name] = pool
+
+def getHTTPClientPool(name):
+    return _clientPools[name]

Deleted: CalendarServer/trunk/twistedcaldav/client/reverseproxy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/client/reverseproxy.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/client/reverseproxy.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,76 +0,0 @@
-##
-# Copyright (c) 2009 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.
-##
-
-__all__ = [
-    "ReverseProxyResource",
-]
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.web2 import iweb
-from twisted.web2.client.http import ClientRequest
-from twisted.web2.resource import LeafResource
-
-from twistedcaldav.client.pool import getHTTPClientPool
-from twistedcaldav.config import config
-from twistedcaldav.log import LoggingMixIn
-
-import urllib
-from zope.interface.declarations import implements
-
-class ReverseProxyResource(LeafResource, LoggingMixIn):
-    """
-    A L{LeafResource} which always performs a reverse proxy operation.
-    """
-    implements(iweb.IResource)
-
-    def __init__(self, poolID, *args, **kwargs):
-        """
-        
-        @param poolID: idenitifier of the pool to use
-        @type poolID: C{str}
-        """
-        
-        self.poolID = poolID
-        self._args   = args
-        self._kwargs = kwargs
-
-    def isCollection(self):
-        return True
-
-    def exists(self):
-        return False
-
-    def renderHTTP(self, request):
-        """
-        Do the reverse proxy request and return the response.
-
-        @param request: the incoming request that needs to be proxied.
-        @type request: L{Request}
-        
-        @return: Deferred L{Response}
-        """
-            
-        self.logger.info("%s %s %s" % (request.method, urllib.unquote(request.uri), "HTTP/%s.%s" % request.clientproto))
-        clientPool = getHTTPClientPool(self.poolID)
-        proxyRequest = ClientRequest(request.method, request.uri, request.headers, request.stream)
-        
-        # Need x-forwarded-(for|host) headers. First strip any existing ones out, then add ours
-        proxyRequest.headers.removeHeader("x-forwarded-host")
-        proxyRequest.headers.removeHeader("x-forwarded-for")
-        proxyRequest.headers.addRawHeader("x-forwarded-host", config.ServerHostName)
-        proxyRequest.headers.addRawHeader("x-forwarded-for", request.remoteAddr.host)
-
-        return clientPool.submitRequest(proxyRequest)

Copied: CalendarServer/trunk/twistedcaldav/client/reverseproxy.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/client/reverseproxy.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/client/reverseproxy.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/client/reverseproxy.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,75 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+__all__ = [
+    "ReverseProxyResource",
+]
+
+from twisted.web2 import iweb
+from twisted.web2.client.http import ClientRequest
+from twisted.web2.resource import LeafResource
+
+from twistedcaldav.client.pool import getHTTPClientPool
+from twistedcaldav.config import config
+from twistedcaldav.log import LoggingMixIn
+
+import urllib
+from zope.interface.declarations import implements
+
+class ReverseProxyResource(LeafResource, LoggingMixIn):
+    """
+    A L{LeafResource} which always performs a reverse proxy operation.
+    """
+    implements(iweb.IResource)
+
+    def __init__(self, poolID, *args, **kwargs):
+        """
+        
+        @param poolID: idenitifier of the pool to use
+        @type poolID: C{str}
+        """
+        
+        self.poolID = poolID
+        self._args   = args
+        self._kwargs = kwargs
+
+    def isCollection(self):
+        return True
+
+    def exists(self):
+        return False
+
+    def renderHTTP(self, request):
+        """
+        Do the reverse proxy request and return the response.
+
+        @param request: the incoming request that needs to be proxied.
+        @type request: L{Request}
+        
+        @return: Deferred L{Response}
+        """
+            
+        self.logger.info("%s %s %s" % (request.method, urllib.unquote(request.uri), "HTTP/%s.%s" % request.clientproto))
+        clientPool = getHTTPClientPool(self.poolID)
+        proxyRequest = ClientRequest(request.method, request.uri, request.headers, request.stream)
+        
+        # Need x-forwarded-(for|host) headers. First strip any existing ones out, then add ours
+        proxyRequest.headers.removeHeader("x-forwarded-host")
+        proxyRequest.headers.removeHeader("x-forwarded-for")
+        proxyRequest.headers.addRawHeader("x-forwarded-host", config.ServerHostName)
+        proxyRequest.headers.addRawHeader("x-forwarded-for", request.remoteAddr.host)
+
+        return clientPool.submitRequest(proxyRequest)

Copied: CalendarServer/trunk/twistedcaldav/database.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/database.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/database.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/database.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,557 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from twisted.enterprise.adbapi import ConnectionPool
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.threadpool import ThreadPool
+
+from twistedcaldav.config import ConfigurationError
+from twistedcaldav.log import Logger
+
+import thread
+
+try:
+    import pgdb
+except:
+    pgdb = None
+#pgdb = None
+
+"""
+Generic ADAPI database access object.
+"""
+
+__all__ = [
+    "AbstractADBAPIDatabase",
+]
+
+log = Logger()
+
+class ConnectionClosingThreadPool(ThreadPool):
+    """
+    A ThreadPool that closes connections for each worker thread
+    """
+    
+    def _worker(self):
+        log.debug("Starting ADBAPI thread: %s" % (thread.get_ident(),))
+        ThreadPool._worker(self)
+        self._closeConnection()
+
+    def _closeConnection(self):
+        
+        tid = thread.get_ident()
+        log.debug("Closing ADBAPI thread: %s" % (tid,))
+
+        conn = self.pool.connections.get(tid)
+        self.pool._close(conn)
+        del self.pool.connections[tid]
+
+class AbstractADBAPIDatabase(object):
+    """
+    A generic SQL database.
+    """
+
+    def __init__(self, dbID, dbapiName, dbapiArgs, persistent, **kwargs):
+        """
+        
+        @param persistent: C{True} if the data in the DB must be perserved during upgrades,
+            C{False} if the DB data can be re-created from an external source.
+        @type persistent: bool
+        """
+        self.dbID = dbID
+        self.dbapiName = dbapiName
+        self.dbapiArgs = dbapiArgs
+        self.dbapikwargs = kwargs
+
+        self.persistent = persistent
+        
+        self.initialized = False
+
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.pool)
+
+    @inlineCallbacks
+    def open(self):
+        """
+        Access the underlying database.
+        @return: a db2 connection object for this index's underlying data store.
+        """
+        if not self.initialized:
+
+            self.pool = ConnectionPool(self.dbapiName, *self.dbapiArgs, **self.dbapikwargs)
+            
+            # sqlite3 is not thread safe which means we have to close the sqlite3 connections in the same thread that
+            # opened them. We need a special thread pool class that has a thread worker function that does a close
+            # when a thread is closed.
+            if self.dbapiName == "sqlite3":
+                self.pool.threadpool.stop()
+                self.pool.threadpool = ConnectionClosingThreadPool(1, 1)
+                self.pool.threadpool.start()
+                self.pool.threadpool.pool = self.pool
+
+            #
+            # Set up the schema
+            #
+            # Create CALDAV table if needed
+
+            test = (yield self._test_schema_table())
+            if test:
+                version = (yield self._db_value_for_sql("select VALUE from CALDAV where KEY = 'SCHEMA_VERSION'"))
+                dbtype = (yield self._db_value_for_sql("select VALUE from CALDAV where KEY = 'TYPE'"))
+
+                if (version != self._db_version()) or (dbtype != self._db_type()):
+
+                    if dbtype != self._db_type():
+                        log.err("Database %s has different type (%s vs. %s)"
+                                % (self.dbID, dbtype, self._db_type()))
+
+                        # Delete this index and start over
+                        yield self._db_remove()
+                        yield self._db_init()
+
+                    elif version != self._db_version():
+                        log.err("Database %s has different schema (v.%s vs. v.%s)"
+                                % (self.dbID, version, self._db_version()))
+                        
+                        # Upgrade the DB
+                        yield self._db_upgrade(version)
+
+            else:
+                yield self._db_init()
+            self.initialized = True
+
+    def close(self):
+        
+        if self.initialized:
+            self.pool.close()
+            self.pool = None
+            self.initialized = False
+
+    @inlineCallbacks
+    def clean(self):
+        
+        if not self.initialized:
+            yield self.open()
+
+        yield self._db_empty_data_tables()
+
+    @inlineCallbacks
+    def execute(self, sql, *query_params):
+        
+        if not self.initialized:
+            yield self.open()
+
+        yield self._db_execute(sql, *query_params)
+
+    @inlineCallbacks
+    def executescript(self, script):
+        
+        if not self.initialized:
+            yield self.open()
+
+        yield self._db_execute_script(script)
+
+    @inlineCallbacks
+    def query(self, sql, *query_params):
+        
+        if not self.initialized:
+            yield self.open()
+
+        result = (yield self._db_all_values_for_sql(sql, *query_params))
+        returnValue(result)
+
+    @inlineCallbacks
+    def queryList(self, sql, *query_params):
+        
+        if not self.initialized:
+            yield self.open()
+
+        result = (yield self._db_values_for_sql(sql, *query_params))
+        returnValue(result)
+
+    @inlineCallbacks
+    def queryOne(self, sql, *query_params):
+        
+        if not self.initialized:
+            yield self.open()
+
+        result = (yield self._db_value_for_sql(sql, *query_params))
+        returnValue(result)
+
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this DB.
+        """
+        raise NotImplementedError
+        
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this DB.
+        """
+        raise NotImplementedError
+        
+    def _test_schema_table(self):
+        return self._test_table("CALDAV")
+
+    @inlineCallbacks
+    def _db_init(self):
+        """
+        Initialise the underlying database tables.
+        """
+        log.msg("Initializing database %s" % (self.dbID,))
+
+        # TODO we need an exclusive lock of some kind here to prevent a race condition
+        # in which multiple processes try to create the tables.
+        
+
+        yield self._db_init_schema_table()
+        yield self._db_init_data_tables()
+        yield self._db_recreate()
+
+    @inlineCallbacks
+    def _db_init_schema_table(self):
+        """
+        Initialise the underlying database tables.
+        @param db_filename: the file name of the index database.
+        @param q:           a database cursor to use.
+        """
+
+        #
+        # CALDAV table keeps track of our schema version and type
+        #
+        yield self._create_table("CALDAV", (
+            ("KEY", "text unique"),
+            ("VALUE", "text unique"),
+        ), True)
+
+        yield self._db_execute(
+            """
+            insert or ignore into CALDAV (KEY, VALUE)
+            values ('SCHEMA_VERSION', :1)
+            """, (self._db_version(),)
+        )
+        yield self._db_execute(
+            """
+            insert or ignore into CALDAV (KEY, VALUE)
+            values ('TYPE', :1)
+            """, (self._db_type(),)
+        )
+
+    def _db_init_data_tables(self):
+        """
+        Initialise the underlying database tables.
+        """
+        raise NotImplementedError
+
+    def _db_empty_data_tables(self):
+        """
+        Delete the database tables.
+        """
+
+        # Implementations can override this to re-create data
+        pass
+        
+    def _db_recreate(self):
+        """
+        Recreate the database tables.
+        """
+
+        # Implementations can override this to re-create data
+        pass
+
+    @inlineCallbacks
+    def _db_upgrade(self, old_version):
+        """
+        Upgrade the database tables.
+        """
+        
+        if self.persistent:
+            yield self._db_upgrade_data_tables(old_version)
+            yield self._db_upgrade_schema()
+        else:
+            # Non-persistent DB's by default can be removed and re-created. However, for simple
+            # DB upgrades they SHOULD override this method and handle those for better performance.
+            yield self._db_remove()
+            yield self._db_init()
+    
+    def _db_upgrade_data_tables(self, old_version):
+        """
+        Upgrade the data from an older version of the DB.
+        """
+        # Persistent DB's MUST override this method and do a proper upgrade. Their data
+        # cannot be thrown away.
+        raise NotImplementedError("Persistent databases MUST support an upgrade method.")
+
+    @inlineCallbacks
+    def _db_upgrade_schema(self):
+        """
+        Upgrade the stored schema version to the current one.
+        """
+        yield self._db_execute("insert or replace into CALDAV (KEY, VALUE) values ('SCHEMA_VERSION', :1)", (self._db_version(),))
+
+    @inlineCallbacks
+    def _db_remove(self):
+        """
+        Remove all database information (all the tables)
+        """
+        yield self._db_remove_data_tables()
+        yield self._db_remove_schema()
+
+    def _db_remove_data_tables(self):
+        """
+        Remove all the data from an older version of the DB.
+        """
+        raise NotImplementedError("Each database must remove its own tables.")
+
+    @inlineCallbacks
+    def _db_remove_schema(self):
+        """
+        Remove the stored schema version table.
+        """
+        yield self._db_execute("drop table if exists CALDAV")
+
+    @inlineCallbacks
+    def _db_all_values_for_sql(self, sql, *query_params):
+        """
+        Execute an SQL query and obtain the resulting values.
+        @param sql: the SQL query to execute.
+        @param query_params: parameters to C{sql}.
+        @return: an interable of values in the first column of each row
+            resulting from executing C{sql} with C{query_params}.
+        @raise AssertionError: if the query yields multiple columns.
+        """
+        
+        sql = self._prepare_statement(sql)
+        results = (yield self.pool.runQuery(sql, *query_params))
+        returnValue(tuple(results))
+
+    @inlineCallbacks
+    def _db_values_for_sql(self, sql, *query_params):
+        """
+        Execute an SQL query and obtain the resulting values.
+
+        @param sql: the SQL query to execute.
+        @param query_params: parameters to C{sql}.
+        @return: an interable of values in the first column of each row
+            resulting from executing C{sql} with C{query_params}.
+        @raise AssertionError: if the query yields multiple columns.
+        """
+        
+        sql = self._prepare_statement(sql)
+        results = (yield self.pool.runQuery(sql, *query_params))
+        returnValue(tuple([row[0] for row in results]))
+
+    @inlineCallbacks
+    def _db_value_for_sql(self, sql, *query_params):
+        """
+        Execute an SQL query and obtain a single value.
+
+        @param sql: the SQL query to execute.
+        @param query_params: parameters to C{sql}.
+        @return: the value resulting from the executing C{sql} with
+            C{query_params}.
+        @raise AssertionError: if the query yields multiple rows or columns.
+        """
+        value = None
+        for row in (yield self._db_values_for_sql(sql, *query_params)):
+            assert value is None, "Multiple values in DB for %s %s" % (sql, query_params)
+            value = row
+        returnValue(value)
+
+    def _db_execute(self, sql, *query_params):
+        """
+        Execute an SQL operation that returns None.
+
+        @param sql: the SQL query to execute.
+        @param query_params: parameters to C{sql}.
+        @return: an iterable of tuples for each row resulting from executing
+            C{sql} with C{query_params}.
+        """
+        
+        sql = self._prepare_statement(sql)
+        return self.pool.runOperation(sql, *query_params)
+
+    """
+    Since different databases support different types of columns and modifiers on those we need to
+    have an "abstract" way of specifying columns in our code and then map the abstract specifiers to
+    the underlying DB's allowed types.
+    
+    Types we can use are:
+    
+    integer
+    text
+    text(n)
+    date
+    serial
+    
+    The " unique" modifier can be appended to any of those.
+    """
+    def _map_column_types(self, type):
+        raise NotImplementedError
+        
+    def _create_table(self, name, columns, ifnotexists=False):
+        raise NotImplementedError
+
+    def _test_table(self, name):
+        raise NotImplementedError
+
+    def _prepare_statement(self, sql):
+        raise NotImplementedError
+        
+class ADBAPISqliteMixin(object):
+
+    @classmethod
+    def _map_column_types(self, coltype):
+        
+        result = ""
+        splits = coltype.split()
+        if splits[0] == "integer":
+            result = "integer"
+        elif splits[0] == "text":
+            result = "text"
+        elif splits[0].startswith("text("):
+            result = splits[0]
+        elif splits[0] == "date":
+            result = "date"
+        elif splits[0] == "serial":
+            result = "integer primary key autoincrement"
+        
+        if len(splits) > 1 and splits[1] == "unique":
+            result += " unique"
+        
+        return result
+
+    @inlineCallbacks
+    def _create_table(self, name, columns, ifnotexists=False):
+        
+        colDefs = ["%s %s" % (colname, self._map_column_types(coltype)) for colname, coltype in columns]
+        statement = "create table %s%s (%s)" % (
+            "if not exists " if ifnotexists else "",
+            name,
+            ", ".join(colDefs),
+        )
+        yield self._db_execute(statement)
+
+    @inlineCallbacks
+    def _test_table(self, name):
+        result = (yield self._db_value_for_sql("""
+        select (1) from SQLITE_MASTER
+         where TYPE = 'table' and NAME = '%s'
+        """ % (name,)))
+        returnValue(result)
+
+    def _prepare_statement(self, sql):
+        # We are going to use the sqlite syntax of :1, :2 etc for our
+        # internal statements so we do not need to remap those
+        return sql
+
+if pgdb:
+
+    class ADBAPIPostgreSQLMixin(object):
+        
+        @classmethod
+        def _map_column_types(self, coltype):
+            
+            result = ""
+            splits = coltype.split()
+            if splits[0] == "integer":
+                result = "integer"
+            elif splits[0] == "text":
+                result = "text"
+            elif splits[0].startswith("text("):
+                result = "char" + splits[0][4:]
+            elif splits[0] == "date":
+                result = "date"
+            elif splits[0] == "serial":
+                result = "serial"
+            
+            if len(splits) > 1 and splits[1] == "unique":
+                result += " unique"
+            
+            return result
+    
+        @inlineCallbacks
+        def _create_table(self, name, columns, ifnotexists=False):
+            
+            colDefs = ["%s %s" % (colname, self._map_column_types(coltype)) for colname, coltype in columns]
+            statement = "create table %s (%s)" % (
+                name,
+                ", ".join(colDefs),
+            )
+            
+            try:
+                yield self._db_execute(statement)
+            except pgdb.DatabaseError:
+                
+                if not ifnotexists:
+                    raise
+                
+                result = (yield self._test_table(name))
+                if not result:
+                    raise 
+    
+        @inlineCallbacks
+        def _test_table(self, name):
+            result = (yield self._db_value_for_sql("""
+            select * from pg_tables
+             where tablename = '%s'
+            """ % (name.lower(),)))
+            returnValue(result)
+    
+        @inlineCallbacks
+        def _db_init_schema_table(self):
+            """
+            Initialise the underlying database tables.
+            @param db_filename: the file name of the index database.
+            @param q:           a database cursor to use.
+            """
+    
+            #
+            # CALDAV table keeps track of our schema version and type
+            #
+            try:
+                yield self._create_table("CALDAV", (
+                    ("KEY", "text unique"),
+                    ("VALUE", "text unique"),
+                ), True)
+    
+                yield self._db_execute(
+                    """
+                    insert into CALDAV (KEY, VALUE)
+                    values ('SCHEMA_VERSION', :1)
+                    """, (self._db_version(),)
+                )
+                yield self._db_execute(
+                    """
+                    insert into CALDAV (KEY, VALUE)
+                    values ('TYPE', :1)
+                    """, (self._db_type(),)
+                )
+            except pgdb.DatabaseError:
+                pass
+    
+        def _prepare_statement(self, sql):
+            # Convert :1, :2 etc format into %s
+            ctr = 1
+            while sql.find(":%d" % (ctr,)) != -1:
+                sql = sql.replace(":%d" % (ctr,), "%s")
+                ctr += 1
+            return sql
+
+else:
+    class ADBAPIPostgreSQLMixin(object):
+        
+        def __init__(self):
+            raise ConfigurationError("PostgreSQL module not available.")

Deleted: CalendarServer/trunk/twistedcaldav/directory/apache.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/apache.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/apache.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,220 +0,0 @@
-##
-# Copyright (c) 2006-2009 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.
-##
-
-"""
-Apache UserFile/GroupFile compatible directory service implementation.
-"""
-
-__all__ = [
-    "BasicDirectoryService",
-    "DigestDirectoryService",
-]
-
-from crypt import crypt
-
-from twisted.python.filepath import FilePath
-from twisted.cred.credentials import UsernamePassword
-
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.directory import UnknownRecordTypeError, DirectoryConfigurationError
-
-class AbstractDirectoryService(DirectoryService):
-    """
-    Abstract Apache-compatible implementation of L{IDirectoryService}.
-    """
-    def __repr__(self):
-        return "<%s %r: %r %r>" % (self.__class__.__name__, self.realmName, self.userFile, self.groupFile)
-
-    def __init__(self, params):
-        defaults = {
-            'realmName' : '',
-            'userFile' : None,
-            'groupFile' : None,
-        }
-        ignored = None
-        params = self.getParams(params, defaults, ignored)
-
-        super(AbstractDirectoryService, self).__init__()
-
-        userFile = params["userFile"]
-        if not userFile:
-            raise DirectoryConfigurationError("Invalid Apache user file name: %r" % (userFile,))
-
-        if userFile and type(userFile) is str:
-            userFile = FilePath(userFile)
-
-        groupFile = params["groupFile"]
-        if groupFile and type(groupFile) is str:
-            groupFile = FilePath(groupFile)
-
-        self.realmName = params["realmName"]
-        self.userFile = userFile
-        self.groupFile = groupFile
-
-    def recordTypes(self):
-        recordTypes = (DirectoryService.recordType_users,)
-        if self.groupFile is not None:
-            recordTypes += (DirectoryService.recordType_groups,)
-        return recordTypes
-
-    def listRecords(self, recordType):
-        for entryShortName, entryData in self.entriesForRecordType(recordType):
-            if recordType == DirectoryService.recordType_users:
-                yield self.userRecordClass(
-                    service       = self,
-                    recordType    = recordType,
-                    shortName     = entryShortName,
-                    cryptPassword = entryData,
-                )
-
-            elif recordType == DirectoryService.recordType_groups:
-                yield GroupRecord(
-                    service    = self,
-                    recordType = recordType,
-                    shortName  = entryShortName,
-                    members    = entryData,
-                )
-
-            else:
-                # Subclass should cover the remaining record types
-                raise UnknownRecordTypeError("Unknown record type: %s" % (recordType,))
-
-    def recordWithShortName(self, recordType, shortName):
-        for entryShortName, entryData in self.entriesForRecordType(recordType):
-            if entryShortName == shortName:
-                if recordType == DirectoryService.recordType_users:
-                    return self.userRecordClass(
-                        service       = self,
-                        recordType    = recordType,
-                        shortName     = entryShortName,
-                        cryptPassword = entryData,
-                    )
-
-                if recordType == DirectoryService.recordType_groups:
-                    return GroupRecord(
-                        service    = self,
-                        recordType = recordType,
-                        shortName  = entryShortName,
-                        members    = entryData,
-                    )
-
-                # Subclass should cover the remaining record types
-                raise UnknownRecordTypeError("Unknown record type: %s" % (recordType,))
-
-        return None
-
-    def entriesForRecordType(self, recordType):
-        if recordType == DirectoryService.recordType_users:
-            recordFile = self.userFile
-        elif recordType == DirectoryService.recordType_groups:
-            recordFile = self.groupFile
-        else:
-            raise UnknownRecordTypeError("Unknown record type: %s" % (recordType,))
-
-        if recordFile is None:
-            return
-
-        try:
-            handle = recordFile.open()
-        except (IOError, OSError):
-            self.log_error("Auth file (for %s) not found: %s" % (recordType, recordFile.path))
-            return
-
-        try:
-            for entry in handle:
-                if entry and entry[0] != "#":
-                    try:
-                        shortName, rest = entry.rstrip("\n").split(":", 1)
-                    except ValueError:
-                        continue
-                    yield shortName, rest
-        finally:
-            handle.close()
-
-class AbstractDirectoryRecord(DirectoryRecord):
-    """
-    Abstract Apache-compatible implementation of L{IDirectoryRecord}.
-    """
-    def __init__(self, service, recordType, shortName):
-        super(AbstractDirectoryRecord, self).__init__(
-            service               = service,
-            recordType            = recordType,
-            guid                  = None,
-            shortNames            = (shortName,),
-        )
-
-class AbstractUserRecord(AbstractDirectoryRecord):
-    def __init__(self, service, recordType, shortName, cryptPassword=None):
-        super(AbstractUserRecord, self).__init__(service, recordType, shortName)
-
-        self._cryptPassword = cryptPassword
-
-    def groups(self):
-        for group in self.service.listRecords(DirectoryService.recordType_groups):
-            for member in group.members():
-                if member == self:
-                    yield group
-                    continue
-
-class BasicUserRecord(AbstractUserRecord):
-    """
-    Apache UserFile implementation of L{IDirectoryRecord}.
-    """
-    def verifyCredentials(self, credentials):
-        if self._cryptPassword in ("", "*", "x"):
-            return False
-
-        if isinstance(credentials, UsernamePassword):
-            return crypt(credentials.password, self._cryptPassword) == self._cryptPassword
-
-        return super(BasicUserRecord, self).verifyCredentials(credentials)
-
-class BasicDirectoryService(AbstractDirectoryService):
-    """
-    Apache UserFile/GroupFile implementation of L{IDirectoryService}.
-    """
-    baseGUID = "DDF1E45C-CADE-4FCD-8AE6-B4B41D72B325"
-    userRecordClass = BasicUserRecord
-
-class DigestUserRecord(AbstractUserRecord):
-    """
-    Apache DigestUserFile implementation of L{IDirectoryRecord}.
-    """
-    def verifyCredentials(self, credentials):
-        raise NotImplementedError()
-
-class DigestDirectoryService(AbstractDirectoryService):
-    """
-    Apache DigestUserFile/GroupFile implementation of L{IDirectoryService}.
-    """
-    baseGUID = "0C719D1B-0A14-4074-8740-6D96A7D0C787"
-    userRecordClass = DigestUserRecord
-
-class GroupRecord(AbstractDirectoryRecord):
-    """
-    Apache GroupFile implementation of L{IDirectoryRecord}.
-    """
-    def __init__(self, service, recordType, shortName, members=()):
-        super(GroupRecord, self).__init__(service, recordType, shortName)
-
-        if type(members) is str:
-            members = tuple(m.strip() for m in members.split(","))
-
-        self._members = members
-
-    def members(self):
-        for shortName in self._members:
-            yield self.service.recordWithShortName(DirectoryService.recordType_users, shortName)

Modified: CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -24,13 +24,7 @@
 ]
 
 import sys
-import time
-from uuid import UUID
 
-from twext.python.plistlib import readPlistFromString
-
-from xml.parsers.expat import ExpatError
-
 import opendirectory
 import dsattributes
 import dsquery
@@ -38,8 +32,9 @@
 from twisted.internet.threads import deferToThread
 from twisted.cred.credentials import UsernamePassword
 from twisted.web2.auth.digest import DigestedCredentials
-from twistedcaldav.config import config
 
+from twistedcaldav.config import config
+from twistedcaldav.directory import augment
 from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
     CachingDirectoryRecord
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
@@ -60,11 +55,6 @@
         """
         @param params: a dictionary containing the following keys:
             node: an OpenDirectory node name to bind to.
-            restrictEnabledRecords: C{True} if a group in the
-              directory is to be used to determine which calendar
-              users are enabled.
-            restrictToGroup: C{str} guid or name of group used to
-              restrict enabled users.
             cacheTimeout: C{int} number of minutes before cache is invalidated.
         @param dosetup: if C{True} then the directory records are initialized,
                         if C{False} they are not.
@@ -73,11 +63,13 @@
 
         defaults = {
             'node' : '/Search',
-            'restrictEnabledRecords' : False,
-            'restrictToGroup' : '',
             'cacheTimeout' : 30,
         }
-        ignored = ('requireComputerRecord',)
+        ignored = (
+            'requireComputerRecord',
+            'restrictEnabledRecords',
+            'restrictToGroup'
+        )
         params = self.getParams(params, defaults, ignored)
 
         super(OpenDirectoryService, self).__init__(params['cacheTimeout'])
@@ -91,16 +83,6 @@
         self.realmName = params['node']
         self.directory = directory
         self.node = params['node']
-        self.restrictEnabledRecords = params['restrictEnabledRecords']
-        self.restrictToGroup = params['restrictToGroup']
-        try:
-            UUID(self.restrictToGroup)
-        except:
-            self.restrictToGUID = False
-        else:
-            self.restrictToGUID = True
-        self.restrictedGUIDs = None
-        self.restrictedTimestamp = 0
         self._records = {}
         self._delayedCalls = set()
 
@@ -413,7 +395,6 @@
                         firstName             = recordFirstName,
                         lastName              = recordLastName,
                         emailAddresses        = recordEmailAddresses,
-                        enabledForCalendaring = True,
                         memberGUIDs           = (),
                     )
                     yield record
@@ -622,69 +603,6 @@
                                % (recordType, recordShortName, recordNodeName))
                 continue
 
-            # Determine enabled state
-            if recordType == self.recordType_groups:
-                enabledForCalendaring = False
-            else:
-                if (
-                    self.restrictEnabledRecords and
-                    config.Scheduling.iMIP.Username != recordShortName
-                ):
-                    if time.time() - self.restrictedTimestamp > self.cacheTimeout:
-                        attributeToMatch = dsattributes.kDS1AttrGeneratedUID if self.restrictToGUID else dsattributes.kDSNAttrRecordName
-                        valueToMatch = self.restrictToGroup
-                        self.log_debug("Doing restricted group membership check")
-                        self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                            self.directory,
-                            attributeToMatch,
-                            valueToMatch,
-                            dsattributes.eDSExact,
-                            False,
-                            dsattributes.kDSStdRecordTypeGroups,
-                            [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
-                        ))
-                        results = lookupMethod(
-                            self.directory,
-                            attributeToMatch,
-                            valueToMatch,
-                            dsattributes.eDSExact,
-                            False,
-                            dsattributes.kDSStdRecordTypeGroups,
-                            [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups,],
-                        )
-
-                        if len(results) == 1:
-                            members = results[0][1].get(dsattributes.kDSNAttrGroupMembers, [])
-                            nestedGroups = results[0][1].get(dsattributes.kDSNAttrNestedGroups, [])
-                        else:
-                            members = []
-                            nestedGroups = []
-                        self.restrictedGUIDs = set(self._expandGroupMembership(members, nestedGroups, returnGroups=True))
-                        self.log_debug("Got %d restricted group members" % (len(self.restrictedGUIDs),))
-                        self.restrictedTimestamp = time.time()
-
-                    enabledForCalendaring = recordGUID in self.restrictedGUIDs
-                else:
-                    enabledForCalendaring = True
-
-            if not enabledForCalendaring:
-                # Some records we want to keep even though they are not enabled for calendaring.
-                # Others we discard.
-                if recordType not in (
-                    self.recordType_users,
-                    self.recordType_groups,
-                ):
-                    self.log_debug(
-                        "Record (%s) %s is not enabled for calendaring"
-                        % (recordType, recordShortName)
-                    )
-                    continue
-
-                self.log_debug(
-                    "Record (%s) %s is not enabled for calendaring but may be used in ACLs"
-                    % (recordType, recordShortName)
-                )
-
             # Special case for groups, which have members.
             if recordType == self.recordType_groups:
                 memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
@@ -711,10 +629,16 @@
                 firstName             = recordFirstName,
                 lastName              = recordLastName,
                 emailAddresses        = recordEmailAddresses,
-                enabledForCalendaring = enabledForCalendaring,
                 memberGUIDs           = memberGUIDs,
             )
-            if enabledForCalendaring:
+
+            # Look up augment information
+            # TODO: this needs to be deferred but for now we hard code the deferred result because
+            # we know it is completing immediately.
+            d = augment.AugmentService.getAugmentRecord(record.guid)
+            d.addCallback(lambda x:record.addAugmentInformation(x))
+
+            if record.enabledForCalendaring:
                 enabledRecords.append(record)
             else:
                 disabledRecords.append(record)
@@ -741,73 +665,6 @@
 
             self.recordCacheForType(recordType).addRecord(record, indexType, origIndexKey)
 
-    def _parseResourceInfo(self, plist, guid, recordType, shortname):
-        """
-        Parse OD ResourceInfo attribute and extract information that the server needs.
-
-        @param plist: the plist that is the attribute value.
-        @type plist: str
-        @param guid: the directory GUID of the record being parsed.
-        @type guid: str
-        @param shortname: the record shortname of the record being parsed.
-        @type shortname: str
-        @return: a C{tuple} of C{bool} for auto-accept, C{str} for proxy GUID, C{str} for read-only proxy GUID.
-        """
-        try:
-            plist = readPlistFromString(plist)
-            wpframework = plist.get("com.apple.WhitePagesFramework", {})
-            autoaccept = wpframework.get("AutoAcceptsInvitation", False)
-            proxy = wpframework.get("CalendaringDelegate", None)
-            read_only_proxy = wpframework.get("ReadOnlyCalendaringDelegate", None)
-        except (ExpatError, AttributeError), e:
-            self.log_error(
-                "Failed to parse ResourceInfo attribute of record (%s)%s (guid=%s): %s\n%s" %
-                (recordType, shortname, guid, e, plist,)
-            )
-            raise ValueError("Invalid ResourceInfo")
-
-        return (autoaccept, proxy, read_only_proxy,)
-
-    def getResourceInfo(self):
-        """
-        Resource information including proxy assignments for resource and
-        locations, as well as auto-schedule settings, used to live in the
-        directory.  This method fetches old resource info for migration
-        purposes.
-        """
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-            dsattributes.kDSNAttrResourceInfo,
-        ]
-
-        for recordType in (dsattributes.kDSStdRecordTypePlaces, dsattributes.kDSStdRecordTypeResources):
-            try:
-                self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
-                    self.directory,
-                    recordType,
-                    attrs,
-                ))
-                results = opendirectory.listAllRecordsWithAttributes_list(
-                    self.directory,
-                    recordType,
-                    attrs,
-                )
-            except opendirectory.ODError, ex:
-                self.log_error("OpenDirectory (node=%s) error: %s" % (self.realmName, str(ex)))
-                raise
-
-            for (recordShortName, value) in results:
-                recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-                resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
-                if resourceInfo is not None:
-                    try:
-                        autoSchedule, proxy, readOnlyProxy = self._parseResourceInfo(resourceInfo,
-                            recordGUID, recordType, recordShortName)
-                    except ValueError:
-                        continue
-                    yield recordGUID, autoSchedule, proxy, readOnlyProxy
-
-
     def isAvailable(self):
         """
         Returns True if all configured directory nodes are accessible, False otherwise
@@ -860,8 +717,7 @@
     """
     def __init__(
         self, service, recordType, guid, nodeName, shortNames, authIDs,
-        fullName, firstName, lastName, emailAddresses,
-        enabledForCalendaring, memberGUIDs,
+        fullName, firstName, lastName, emailAddresses, memberGUIDs,
     ):
         super(OpenDirectoryRecord, self).__init__(
             service               = service,

Copied: CalendarServer/trunk/twistedcaldav/directory/augment.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/directory/augment.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/augment.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/augment.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,353 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin,\
+    ADBAPIPostgreSQLMixin
+from twistedcaldav.directory.xmlaugmentsparser import XMLAugmentsParser
+import copy
+import time
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+class AugmentRecord(object):
+    """
+    Augmented directory record information
+    """
+
+    def __init__(
+        self,
+        guid,
+        enabled=False,
+        hostedAt="",
+        enabledForCalendaring=False,
+        autoSchedule=False,
+    ):
+        self.guid = guid
+        self.enabled = enabled
+        self.hostedAt = hostedAt
+        self.enabledForCalendaring = enabledForCalendaring
+        self.autoSchedule = autoSchedule
+
+class AugmentDB(object):
+    """
+    Abstract base class for an augment record database.
+    """
+    
+    def __init__(self):
+        pass
+    
+    @inlineCallbacks
+    def getAugmentRecord(self, guid):
+        """
+        Get an AugmentRecord for the specified GUID or the default.
+
+        @param guid: directory GUID to lookup
+        @type guid: C{str}
+        
+        @return: L{Deferred}
+        """
+        
+        result = (yield self._lookupAugmentRecord(guid))
+        if result is None:
+            if not hasattr(self, "_defaultRecord"):
+                self._defaultRecord = (yield self._lookupAugmentRecord("Default"))
+            if self._defaultRecord is not None:
+                result = copy.deepcopy(self._defaultRecord)
+                result.guid = guid
+        returnValue(result)
+
+    @inlineCallbacks
+    def getAllGUIDs(self):
+        """
+        Get all AugmentRecord GUIDs.
+
+        @return: L{Deferred}
+        """
+        
+        raise NotImplementedError("Child class must define this.")
+
+    def _lookupAugmentRecord(self, guid):
+        """
+        Get an AugmentRecord for the specified GUID.
+
+        @param guid: directory GUID to lookup
+        @type guid: C{str}
+        
+        @return: L{Deferred}
+        """
+        
+        raise NotImplementedError("Child class must define this.")
+
+    def refresh(self):
+        """
+        Refresh any cached data.
+        """
+        pass
+        
+AugmentService = AugmentDB()   # Global augment service
+
+
+class AugmentXMLDB(AugmentDB):
+    """
+    XMLFile based augment database implementation.
+    """
+    
+    def __init__(self, xmlFiles, cacheTimeout=30):
+        
+        self.xmlFiles = xmlFiles
+        self.cacheTimeout = cacheTimeout * 60 # Value is mins we want secs
+        self.lastCached = 0
+        self.db = {}
+        
+        try:
+            self.db = self._parseXML()
+        except RuntimeError:
+            log.error("Failed to parse XML augments file - fatal error on startup")
+            raise
+            
+        self.lastCached = time.time()
+
+    @inlineCallbacks
+    def getAllGUIDs(self):
+        """
+        Get all AugmentRecord GUIDs.
+
+        @return: L{Deferred}
+        """
+        
+        return succeed(self.db.keys())
+
+    def _lookupAugmentRecord(self, guid):
+        """
+        Get an AugmentRecord for the specified GUID.
+
+        @param guid: directory GUID to lookup
+        @type guid: C{str}
+        
+        @return: L{Deferred}
+        """
+        
+        # May need to re-cache
+        if self.lastCached + self.cacheTimeout <= time.time():
+            self.refresh()
+            
+        return succeed(self.db.get(guid))
+
+    def refresh(self):
+        """
+        Refresh any cached data.
+        """
+        try:
+            self.db = self._parseXML()
+        except RuntimeError:
+            log.error("Failed to parse XML augments file during cache refresh - ignoring")
+        self.lastCached = time.time()
+
+    def _parseXML(self):
+        
+        # Do each file
+        results = {}
+        for xmlFile in self.xmlFiles:
+            
+            # Creating a parser does the parse
+            XMLAugmentsParser(xmlFile, results)
+        
+        return results
+
+class AugmentADAPI(AugmentDB, AbstractADBAPIDatabase):
+    """
+    DBAPI based augment database implementation.
+    """
+
+    schema_version = "1"
+    schema_type    = "AugmentDB"
+    
+    def __init__(self, dbID, dbapiName, dbapiArgs, **kwargs):
+        
+        self.cachedPartitions = {}
+        self.cachedHostedAt = {}
+        
+        AbstractADBAPIDatabase.__init__(self, dbID, dbapiName, dbapiArgs, True, **kwargs)
+        
+    @inlineCallbacks
+    def getAllGUIDs(self):
+        """
+        Get all AugmentRecord GUIDs.
+
+        @return: L{Deferred}
+        """
+        
+        # Query for the record information
+        results = (yield self.queryList("select GUID from AUGMENTS", ()))
+        returnValue(results)
+
+    @inlineCallbacks
+    def _lookupAugmentRecord(self, guid):
+        """
+        Get an AugmentRecord for the specified GUID.
+
+        @param guid: directory GUID to lookup
+        @type guid: C{str}
+
+        @return: L{Deferred}
+        """
+        
+        # Query for the record information
+        results = (yield self.query("select GUID, ENABLED, PARTITIONID, CALENDARING, AUTOSCHEDULE from AUGMENTS where GUID = :1", (guid,)))
+        if not results:
+            returnValue(None)
+        else:
+            guid, enabled, partitionid, enabdledForCalendaring, autoSchedule = results[0]
+            
+            record = AugmentRecord(
+                guid = guid,
+                enabled = enabled == "T",
+                hostedAt = (yield self._getPartition(partitionid)),
+                enabledForCalendaring = enabdledForCalendaring == "T",
+                autoSchedule = autoSchedule == "T",
+            )
+            
+            returnValue(record)
+
+    @inlineCallbacks
+    def addAugmentRecord(self, record, update=False):
+
+        partitionid = (yield self._getPartitionID(record.hostedAt))
+        
+        if update:
+            yield self.execute(
+                """update AUGMENTS set
+                (GUID, ENABLED, PARTITIONID, CALENDARING, AUTOSCHEDULE) =
+                (:1, :2, :3, :4, :5) where GUID = :6""",
+                (
+                    record.guid,
+                    "T" if record.enabled else "F",
+                    partitionid,
+                    "T" if record.enabledForCalendaring else "F",
+                    "T" if record.autoSchedule else "F",
+                    record.guid,
+                )
+            )
+        else:
+            yield self.execute(
+                """insert into AUGMENTS
+                (GUID, ENABLED, PARTITIONID, CALENDARING, AUTOSCHEDULE)
+                values (:1, :2, :3, :4, :5)""",
+                (
+                    record.guid,
+                    "T" if record.enabled else "F",
+                    partitionid,
+                    "T" if record.enabledForCalendaring else "F",
+                    "T" if record.autoSchedule else "F",
+                )
+            )
+
+    def removeAugmentRecord(self, guid):
+
+        return self.query("delete from AUGMENTS where GUID = :1", (guid,))
+
+    @inlineCallbacks
+    def _getPartitionID(self, hostedat, createIfMissing=True):
+        
+        # We will use a cache for these as we do not expect changes whilst running
+        try:
+            returnValue(self.cachedHostedAt[hostedat])
+        except KeyError:
+            pass
+
+        partitionid = (yield self.queryOne("select PARTITIONID from PARTITIONS where HOSTEDAT = :1", (hostedat,)))
+        if partitionid == None:
+            yield self.execute("insert into PARTITIONS (HOSTEDAT) values (:1)", (hostedat,))
+            partitionid = (yield self.queryOne("select PARTITIONID from PARTITIONS where HOSTEDAT = :1", (hostedat,)))
+        self.cachedHostedAt[hostedat] = partitionid
+        returnValue(partitionid)
+
+    @inlineCallbacks
+    def _getPartition(self, partitionid):
+        
+        # We will use a cache for these as we do not expect changes whilst running
+        try:
+            returnValue(self.cachedPartitions[partitionid])
+        except KeyError:
+            pass
+
+        partition = (yield self.queryOne("select HOSTEDAT from PARTITIONS where PARTITIONID = :1", (partitionid,)))
+        self.cachedPartitions[partitionid] = partition
+        returnValue(partition)
+
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this index.
+        """
+        return AugmentADAPI.schema_version
+        
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this index.
+        """
+        return AugmentADAPI.schema_type
+    
+    @inlineCallbacks
+    def _db_init_data_tables(self):
+        """
+        Initialize the underlying database tables.
+        """
+
+        #
+        # TESTTYPE table
+        #
+        yield self._create_table("AUGMENTS", (
+            ("GUID",         "text unique"),
+            ("ENABLED",      "text(1)"),
+            ("PARTITIONID",  "text"),
+            ("CALENDARING",  "text(1)"),
+            ("AUTOSCHEDULE", "text(1)"),
+        ))
+
+        yield self._create_table("PARTITIONS", (
+            ("PARTITIONID",   "serial"),
+            ("HOSTEDAT",      "text"),
+        ))
+
+    @inlineCallbacks
+    def _db_empty_data_tables(self):
+        yield self._db_execute("delete from AUGMENTS")
+        yield self._db_execute("delete from PARTITIONS")
+
+class AugmentSqliteDB(ADBAPISqliteMixin, AugmentADAPI):
+    """
+    Sqlite based augment database implementation.
+    """
+
+    def __init__(self, dbpath):
+        
+        ADBAPISqliteMixin.__init__(self)
+        AugmentADAPI.__init__(self, "Augments", "sqlite3", (dbpath,))
+
+class AugmentPostgreSQLDB(ADBAPIPostgreSQLMixin, AugmentADAPI):
+    """
+    PostgreSQL based augment database implementation.
+    """
+
+    def __init__(self, host, database, user=None, password=None):
+        
+        ADBAPIPostgreSQLMixin.__init__(self)
+        AugmentADAPI.__init__(self, "Augments", "pgdb", (), host=host, database=database, user=user, password=password,)
+

Modified: CalendarServer/trunk/twistedcaldav/directory/cachingdirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/cachingdirectory.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/cachingdirectory.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.directory.test.test_cachedirectory -*-
 ##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2010 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.
@@ -76,7 +76,7 @@
     def addRecord(self, record, indexType, indexKey, useMemcache=True,
         neverExpire=False):
 
-        useMemcache = useMemcache and config.Memcached.ClientEnabled
+        useMemcache = useMemcache and config.Memcached.Pools.Default.ClientEnabled
         if neverExpire:
             record.neverExpire()
 
@@ -156,7 +156,7 @@
     def _getMemcacheClient(self, refresh=False):
         if refresh or not hasattr(self, "memcacheClient"):
             self.memcacheClient = memcacheclient.ClientFactory.getClient(['%s:%s' %
-                (config.Memcached.BindAddress, config.Memcached.Port)],
+                (config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port)],
                 debug=0, pickleProtocol=2)
         return self.memcacheClient
 
@@ -289,7 +289,7 @@
                 pass
             
             # Check memcache
-            if config.Memcached.ClientEnabled:
+            if config.Memcached.Pools.Default.ClientEnabled:
                 key = "dir|%s|%s" % (indexType, indexKey)
                 self.log_debug("Memcache: checking %s" % (key,))
 
@@ -332,7 +332,7 @@
             self.log_debug("Failed to fault record for attribute '%s' with value '%s'" % (indexType, indexKey,))
             self._disabledKeys[indexType][indexKey] = time.time()
 
-            if config.Memcached.ClientEnabled:
+            if config.Memcached.Pools.Default.ClientEnabled:
                 self.log_debug("Memcache: storing (negative) %s" % (key,))
                 try:
                     self.memcacheSet("-%s" % (key,), 1)
@@ -349,9 +349,10 @@
 class CachingDirectoryRecord(DirectoryRecord):
 
     def __init__(
-        self, service, recordType, guid, shortNames=(), authIDs=set(),
+        self, service, recordType, guid,
+        shortNames=(), authIDs=set(),
         fullName=None, firstName=None, lastName=None, emailAddresses=set(),
-        enabledForCalendaring=None, uid=None,
+        uid=None,
     ):
         super(CachingDirectoryRecord, self).__init__(
             service               = service,
@@ -363,7 +364,6 @@
             firstName             = firstName,
             lastName              = lastName,
             emailAddresses        = emailAddresses,
-            enabledForCalendaring = enabledForCalendaring,
             uid                   = uid,
         )
         

Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -20,7 +20,10 @@
 
 __all__ = [
     "CalendarUserProxyPrincipalResource",
-    "CalendarUserProxyDatabase",
+    "ProxyDB",
+    "ProxyDBService",
+    "ProxySqliteDB",
+    "ProxyPostgreSQLDB",
 ]
 
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue
@@ -32,16 +35,16 @@
 from twisted.web2.dav.noneprops import NonePropertyStore
 
 from twistedcaldav.config import config
+from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin,\
+    ADBAPIPostgreSQLMixin
 from twistedcaldav.extensions import DAVFile, DAVPrincipalResource
 from twistedcaldav.extensions import ReadOnlyWritePropertiesResourceMixIn
 from twistedcaldav.memcacher import Memcacher
 from twistedcaldav.resource import CalDAVComplianceMixIn
 from twistedcaldav.directory.util import NotFilePath
-from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
 from twistedcaldav.log import LoggingMixIn
 
 import itertools
-import os
 import time
 
 class PermissionsMixIn (ReadOnlyWritePropertiesResourceMixIn):
@@ -120,14 +123,10 @@
         """
         Return the SQL database for this group principal.
 
-        @return: the L{CalendarUserProxyDatabase} for the principal collection.
+        @return: the L{ProxyDB} for the principal collection.
         """
+        return ProxyDBService
 
-        # The db is located in the principal collection root
-        if not hasattr(self.pcollection, "calendar_user_proxy_db"):
-            setattr(self.pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(config.DataRoot))
-        return self.pcollection.calendar_user_proxy_db
-
     def resourceType(self):
         if self.proxyType == "calendar-proxy-read":
             return davxml.ResourceType.calendarproxyread
@@ -335,7 +334,7 @@
                 found.append(p)
                 # Make sure any outstanding deletion timer entries for
                 # existing principals are removed
-                yield self._index()._memcacher.clearDeletionTimer(uid)
+                yield self._index().refreshPrincipal(uid)
             else:
                 missing.append(uid)
 
@@ -364,7 +363,7 @@
         ])
         return d
 
-class CalendarUserProxyDatabase(AbstractSQLDatabase, LoggingMixIn):
+class ProxyDB(AbstractADBAPIDatabase, LoggingMixIn):
     """
     A database to maintain calendar user proxy group memberships.
 
@@ -376,11 +375,9 @@
 
     """
 
-    dbType = "CALENDARUSERPROXY"
-    dbFilename = "calendaruserproxy.sqlite"
-    dbOldFilename = db_prefix + "calendaruserproxy"
-    dbFormatVersion = "4"
-
+    schema_version = "4"
+    schema_type    = "ProxyDB"
+    
     class ProxyDBMemcacher(Memcacher):
         
         def setMembers(self, guid, members):
@@ -446,11 +443,10 @@
                 theTime = int(time.time())
             return theTime
 
-    def __init__(self, path):
-        path = os.path.join(path, CalendarUserProxyDatabase.dbFilename)
-        super(CalendarUserProxyDatabase, self).__init__(path, True)
+    def __init__(self, dbID, dbapiName, dbapiArgs, **kwargs):
+        AbstractADBAPIDatabase.__init__(self, dbID, dbapiName, dbapiArgs, True, **kwargs)
         
-        self._memcacher = CalendarUserProxyDatabase.ProxyDBMemcacher("proxyDB")
+        self._memcacher = ProxyDB.ProxyDBMemcacher("ProxyDB")
 
     @inlineCallbacks
     def setGroupMembers(self, principalUID, members):
@@ -467,17 +463,19 @@
             current_members = ()
         current_members = set(current_members)
 
-        self.setGroupMembersInDatabase(principalUID, members)
-
-        # Update cache
+        # Find changes
         update_members = set(members)
-        
         remove_members = current_members.difference(update_members)
         add_members = update_members.difference(current_members)
+
+        yield self.changeGroupMembersInDatabase(principalUID, add_members, remove_members)
+
+        # Update cache
         for member in itertools.chain(remove_members, add_members,):
             yield self._memcacher.deleteMembership(member)
         yield self._memcacher.deleteMember(principalUID)
 
+    @inlineCallbacks
     def setGroupMembersInDatabase(self, principalUID, members):
         """
         A blocking call to add a group membership record in the database.
@@ -486,11 +484,25 @@
         @param members: a list UIDs of principals that are members of this group.
         """
         # Remove what is there, then add it back.
-        self._delete_from_db(principalUID)
-        self._add_to_db(principalUID, members)
-        self._db_commit()
+        yield self._delete_from_db(principalUID)
+        yield self._add_to_db(principalUID, members)
         
     @inlineCallbacks
+    def changeGroupMembersInDatabase(self, principalUID, addMembers, removeMembers):
+        """
+        A blocking call to add a group membership record in the database.
+
+        @param principalUID: the UID of the group principal to add.
+        @param addMembers: a list UIDs of principals to be added as members of this group.
+        @param removeMembers: a list UIDs of principals to be removed as members of this group.
+        """
+        # Remove what is there, then add it back.
+        for member in removeMembers:
+            yield self._delete_from_db_one(principalUID, member)
+        for member in addMembers:
+            yield self._add_to_db_one(principalUID, member)
+        
+    @inlineCallbacks
     def removeGroup(self, principalUID):
         """
         Remove a group membership record.
@@ -501,8 +513,7 @@
         # Need to get the members before we do the delete
         members = yield self.getMembers(principalUID)
 
-        self._delete_from_db(principalUID)
-        self._db_commit()
+        yield self._delete_from_db(principalUID)
         
         # Update cache
         if members:
@@ -536,7 +547,7 @@
                 # No timer was previously set
                 self.log_debug("Delaying removal of missing proxy principal '%s'"
                                % (principalUID,))
-                self._memcacher.setDeletionTimer(principalUID, delay=delay)
+                yield self._memcacher.setDeletionTimer(principalUID, delay=delay)
                 returnValue(None)
 
         self.log_warn("Removing missing proxy principal for '%s'"
@@ -544,7 +555,7 @@
 
         for suffix in ("calendar-proxy-read", "calendar-proxy-write",):
             groupUID = "%s#%s" % (principalUID, suffix,)
-            self._delete_from_db(groupUID)
+            yield self._delete_from_db(groupUID)
 
             # Update cache
             members = yield self.getMembers(groupUID)
@@ -557,11 +568,20 @@
         for groupUID in memberships:
             yield self._memcacher.deleteMember(groupUID)
 
-        self._delete_from_db_member(principalUID)
+        yield self._delete_from_db_member(principalUID)
         yield self._memcacher.deleteMembership(principalUID)
-        self._db_commit()
-        self._memcacher.clearDeletionTimer(principalUID)
+        yield self._memcacher.clearDeletionTimer(principalUID)
 
+    def refreshPrincipal(self, principalUID):
+        """
+        Bring back to life a principal that was previously deleted.
+
+        @param principalUID:
+        @type principalUID:
+        """
+        
+        return self._memcacher.clearDeletionTimer(principalUID)
+
     def getMembers(self, principalUID):
         """
         Return the list of group member UIDs for the specified principal.
@@ -573,12 +593,14 @@
                 return members
 
             # Cache miss; compute members and update cache
-            members = set([
-                row[0] for row in
-                self._db_execute("select MEMBER from GROUPS where GROUPNAME = :1", principalUID)
-            ])
-            d = self._memcacher.setMembers(principalUID, members)
-            d.addCallback(lambda _: members)
+            def gotMembersFromDB(dbmembers):
+                members = set([row[0] for row in dbmembers])
+                d = self._memcacher.setMembers(principalUID, members)
+                d.addCallback(lambda _: members)
+                return d
+
+            d =  self.query("select MEMBER from GROUPS where GROUPNAME = :1", (principalUID,))
+            d.addCallback(gotMembersFromDB)
             return d
 
         d = self._memcacher.getMembers(principalUID)
@@ -596,18 +618,21 @@
                 return memberships
 
             # Cache miss; compute memberships and update cache
-            memberships = set([
-                row[0] for row in
-                self._db_execute("select GROUPNAME from GROUPS where MEMBER = :1", principalUID)
-            ])
-            d = self._memcacher.setMemberships(principalUID, memberships)
-            d.addCallback(lambda _: memberships)
+            def gotMembershipsFromDB(dbmemberships):
+                memberships = set([row[0] for row in dbmemberships])
+                d = self._memcacher.setMemberships(principalUID, memberships)
+                d.addCallback(lambda _: memberships)
+                return d
+
+            d =  self.query("select GROUPNAME from GROUPS where MEMBER = :1", (principalUID,))
+            d.addCallback(gotMembershipsFromDB)
             return d
 
         d = self._memcacher.getMemberships(principalUID)
         d.addCallback(gotCachedMemberships)
         return d
 
+    @inlineCallbacks
     def _add_to_db(self, principalUID, members):
         """
         Insert the specified entry into the database.
@@ -616,42 +641,66 @@
         @param members: a list of UIDs or principals that are members of this group.
         """
         for member in members:
-            self._db_execute(
+            yield self.execute(
                 """
                 insert into GROUPS (GROUPNAME, MEMBER)
                 values (:1, :2)
-                """, principalUID, member
+                """, (principalUID, member,)
             )
 
+    def _add_to_db_one(self, principalUID, memberUID):
+        """
+        Insert the specified entry into the database.
+
+        @param principalUID: the UID of the group principal to add.
+        @param memberUID: the UID of the principal that is being added as a member of this group.
+        """
+        return self.execute(
+            """
+            insert into GROUPS (GROUPNAME, MEMBER)
+            values (:1, :2)
+            """, (principalUID, memberUID,)
+        )
+
     def _delete_from_db(self, principalUID):
         """
         Deletes the specified entry from the database.
 
         @param principalUID: the UID of the group principal to remove.
         """
-        self._db_execute("delete from GROUPS where GROUPNAME = :1", principalUID)
+        return self.execute("delete from GROUPS where GROUPNAME = :1", (principalUID,))
 
+    def _delete_from_db_one(self, principalUID, memberUID):
+        """
+        Deletes the specified entry from the database.
+
+        @param principalUID: the UID of the group principal to remove.
+        @param memberUID: the UID of the principal that is being removed as a member of this group.
+        """
+        return self.execute("delete from GROUPS where GROUPNAME = :1 and MEMBER = :2", (principalUID, memberUID,))
+
     def _delete_from_db_member(self, principalUID):
         """
         Deletes the specified member entry from the database.
 
         @param principalUID: the UID of the member principal to remove.
         """
-        self._db_execute("delete from GROUPS where MEMBER = :1", principalUID)
+        return self.execute("delete from GROUPS where MEMBER = :1", (principalUID,))
 
     def _db_version(self):
         """
         @return: the schema version assigned to this index.
         """
-        return CalendarUserProxyDatabase.dbFormatVersion
+        return ProxyDB.schema_version
 
     def _db_type(self):
         """
         @return: the collection type assigned to this index.
         """
-        return CalendarUserProxyDatabase.dbType
+        return ProxyDB.schema_type
 
-    def _db_init_data_tables(self, q):
+    @inlineCallbacks
+    def _db_init_data_tables(self):
         """
         Initialise the underlying database tables.
         @param q:           a database cursor to use.
@@ -660,48 +709,90 @@
         #
         # GROUPS table
         #
-        q.execute(
+        yield self._create_table("GROUPS", (
+            ("GROUPNAME", "text"),
+            ("MEMBER",    "text"),
+        ))
+
+        yield self._db_execute(
             """
-            create table GROUPS (
-                GROUPNAME   text,
-                MEMBER      text
-            )
-            """
-        )
-        q.execute(
-            """
             create index GROUPNAMES on GROUPS (GROUPNAME)
             """
         )
-        q.execute(
+        yield self._db_execute(
             """
             create index MEMBERS on GROUPS (MEMBER)
             """
         )
 
-    def _db_upgrade_data_tables(self, q, old_version):
+    @inlineCallbacks
+    def _db_upgrade_data_tables(self, old_version):
         """
         Upgrade the data from an older version of the DB.
-        @param q: a database cursor to use.
         @param old_version: existing DB's version number
         @type old_version: str
         """
 
         # Add index if old version is less than "4"
         if int(old_version) < 4:
-            q.execute(
+            yield self._db_execute(
                 """
                 create index GROUPNAMES on GROUPS (GROUPNAME)
                 """
             )
-            q.execute(
+            yield self._db_execute(
                 """
                 create index MEMBERS on GROUPS (MEMBER)
                 """
             )
 
+    def _db_empty_data_tables(self):
+        """
+        Empty the underlying database tables.
+        @param q:           a database cursor to use.
+        """
 
+        #
+        # GROUPS table
+        #
+        return self._db_execute("delete from GROUPS")
 
+    @inlineCallbacks
+    def clean(self):
+        
+        if not self.initialized:
+            yield self.open()
+
+        for group in [row[0] for row in (yield self.query("select GROUPNAME from GROUPS"))]:
+            self.removeGroup(group)
+        
+        yield super(ProxyDB, self).clean()
+
+
+ProxyDBService = None   # Global proxyDB service
+
+
+class ProxySqliteDB(ADBAPISqliteMixin, ProxyDB):
+    """
+    Sqlite based proxy database implementation.
+    """
+
+    def __init__(self, dbpath):
+        
+        ADBAPISqliteMixin.__init__(self)
+        ProxyDB.__init__(self, "Proxies", "sqlite3", (dbpath,))
+
+class ProxyPostgreSQLDB(ADBAPIPostgreSQLMixin, ProxyDB):
+    """
+    PostgreSQL based augment database implementation.
+    """
+
+    def __init__(self, host, database, user=None, password=None):
+        
+        ADBAPIPostgreSQLMixin.__init__(self, )
+        ProxyDB.__init__(self, "Proxies", "pgdb", (), host=host, database=database, user=user, password=password,)
+
+
 ##
 # Utilities
 ##

Copied: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxyloader.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/directory/calendaruserproxyloader.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxyloader.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxyloader.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,137 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from xml.etree.ElementTree import ElementTree
+from xml.parsers.expat import ExpatError
+from twistedcaldav.directory import calendaruserproxy
+from twisted.internet.defer import inlineCallbacks
+import types
+
+"""
+XML based calendar user proxy loader.
+"""
+
+__all__ = [
+    "XMLCalendarUserProxyLoader",
+]
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+ELEMENT_PROXIES           = "proxies"
+ELEMENT_RECORD            = "record"
+
+ELEMENT_GUID              = "guid"
+ELEMENT_PROXIES           = "proxies"
+ELEMENT_READ_ONLY_PROXIES = "read-only-proxies"
+ELEMENT_MEMBER            = "member"
+
+ATTRIBUTE_REPEAT          = "repeat"
+
+class XMLCalendarUserProxyLoader(object):
+    """
+    XML calendar user proxy configuration file parser and loader.
+    """
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+    def __init__(self, xmlFile):
+
+        self.items = []
+        self.xmlFile = xmlFile
+
+        # Read in XML
+        try:
+            tree = ElementTree(file=self.xmlFile)
+        except ExpatError, e:
+            log.error("Unable to parse file '%s' because: %s" % (self.xmlFile, e,), raiseException=RuntimeError)
+
+        # Verify that top-level element is correct
+        proxies_node = tree.getroot()
+        if proxies_node.tag != ELEMENT_PROXIES:
+            log.error("Ignoring file '%s' because it is not a proxies file" % (self.xmlFile,), raiseException=RuntimeError)
+
+        self._parseXML(proxies_node)
+
+    def _parseXML(self, rootnode):
+        """
+        Parse the XML root node from the augments configuration document.
+        @param rootnode: the L{Element} to parse.
+        """
+        for child in rootnode.getchildren():
+            
+            if child.tag != ELEMENT_RECORD:
+                log.error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, self.xmlFile,), raiseException=RuntimeError)
+
+            repeat = int(child.get(ATTRIBUTE_REPEAT, "1"))
+
+            guid = None
+            write_proxies = set()
+            read_proxies = set()
+            for node in child.getchildren():
+                
+                if node.tag == ELEMENT_GUID:
+                    guid = node.text
+
+                elif node.tag in (
+                    ELEMENT_PROXIES,
+                    ELEMENT_READ_ONLY_PROXIES,
+                ):
+                    self._parseMembers(node, write_proxies if node.tag == ELEMENT_PROXIES else read_proxies)
+                else:
+                    log.error("Invalid element '%s' in proxies file: '%s'" % (node.tag, self.xmlFile,), raiseException=RuntimeError)
+                    
+            # Must have at least a guid
+            if not guid:
+                log.error("Invalid record '%s' without a guid in proxies file: '%s'" % (child, self.xmlFile,), raiseException=RuntimeError)
+                
+            if repeat > 1:
+                for i in xrange(1, repeat+1):
+                    self._buildRecord(guid, write_proxies, read_proxies, i)
+            else:
+                self._buildRecord(guid, write_proxies, read_proxies)
+
+    def _parseMembers(self, node, addto):
+        for child in node.getchildren():
+            if child.tag == ELEMENT_MEMBER:
+                addto.add(child.text)
+    
+    def _buildRecord(self, guid, write_proxies, read_proxies, count=None):
+
+        def expandCount(value, count):
+            
+            if type(value) in types.StringTypes:
+                return value % (count,) if count and "%" in value else value
+            else:
+                return value
+        
+        guid = expandCount(guid, count)
+        write_proxies = set([expandCount(member, count) for member in write_proxies])
+        read_proxies = set([expandCount(member, count) for member in read_proxies])
+            
+        self.items.append((guid, write_proxies, read_proxies,))
+
+    @inlineCallbacks
+    def updateProxyDB(self):
+        
+        db = calendaruserproxy.ProxyDBService
+        for item in self.items:
+            guid, write_proxies, read_proxies = item
+            for proxy in write_proxies:
+                yield db.setGroupMembers("%s#%s" % (guid, "calendar-proxy-write"), (proxy,))
+            for proxy in read_proxies:
+                yield db.setGroupMembers("%s#%s" % (guid, "calendar-proxy-read"), (proxy,))

Modified: CalendarServer/trunk/twistedcaldav/directory/digest.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/digest.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/digest.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -163,7 +163,7 @@
             raise AssertionError("nonce value already cached in credentials database: %s" % (c,))
 
         # The database record is a tuple of (client ip, nonce-count, timestamp)
-        yield self.db.set(c, (peer.host, 0, time.time()))
+        yield self.db.set(c, (str(peer.host), 0, time.time()))
 
         challenge = {
             'nonce': c,
@@ -262,7 +262,7 @@
         """
 
         nonce = auth.get('nonce')
-        clientip = request.remoteAddr.host
+        clientip = request.forwarded_for if hasattr(request, "forwarded_for") else str(request.remoteAddr.host)
         nonce_count = auth.get('nc')
 
         # First check we have this nonce

Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.directory.test -*-
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -15,6 +15,7 @@
 # limitations under the License.
 ##
 
+
 """
 Generic directory service classes.
 """
@@ -37,9 +38,11 @@
 from twisted.web2.dav.auth import IPrincipalCredentials
 from twisted.internet.defer import succeed
 
+from twistedcaldav.config import config
 from twistedcaldav.log import LoggingMixIn
 from twistedcaldav.directory.idirectory import IDirectoryService, IDirectoryRecord
 from twistedcaldav.directory.util import uuidFromName
+from twistedcaldav.partitions import partitions
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
 
 class DirectoryService(LoggingMixIn):
@@ -150,7 +153,7 @@
             record = self.recordWithGUID(guid)
         elif address.startswith("mailto:"):
             for record in self.allRecords():
-                if address in record.calendarUserAddresses:
+                if address[7:] in record.emailAddresses:
                     break
             else:
                 return None
@@ -282,20 +285,21 @@
     implements(IDirectoryRecord)
 
     def __repr__(self):
-        return "<%s[%s@%s(%s)] %s(%s) %r>" % (
+        return "<%s[%s@%s(%s)] %s(%s) %r @ %s>" % (
             self.__class__.__name__,
             self.recordType,
             self.service.guid,
             self.service.realmName,
             self.guid,
             ",".join(self.shortNames),
-            self.fullName
+            self.fullName,
+            self.hostedAt,
         )
 
     def __init__(
-        self, service, recordType, guid, shortNames=(), authIDs=set(), fullName=None,
+        self, service, recordType, guid,
+        shortNames=(), authIDs=set(), fullName=None,
         firstName=None, lastName=None, emailAddresses=set(),
-        enabledForCalendaring=None,
         uid=None,
     ):
         assert service.realmName is not None
@@ -311,28 +315,21 @@
         if fullName is None:
             fullName = ""
 
-        if enabledForCalendaring is None:
-            if recordType == service.recordType_groups:
-                enabledForCalendaring = False
-            else:
-                enabledForCalendaring = True
-
-        if enabledForCalendaring and recordType == service.recordType_groups:
-            raise AssertionError("Groups may not be enabled for calendaring")
-
         self.service               = service
         self.recordType            = recordType
         self.guid                  = guid
         self.uid                   = uid
+        self.enabled               = False
+        self.hostedAt              = ""
         self.shortNames            = shortNames
         self.authIDs               = authIDs
         self.fullName              = fullName
         self.firstName             = firstName
         self.lastName              = lastName
         self.emailAddresses        = emailAddresses
-        self.enabledForCalendaring = enabledForCalendaring
+        self.enabledForCalendaring = False
+        self.autoSchedule          = False
 
-
     def get_calendarUserAddresses(self):
         """
         Dynamically construct a calendarUserAddresses attribute which describes
@@ -350,7 +347,6 @@
 
     calendarUserAddresses = property(get_calendarUserAddresses)
 
-
     def __cmp__(self, other):
         if not isinstance(other, DirectoryRecord):
             return NotImplemented
@@ -364,11 +360,28 @@
     def __hash__(self):
         h = hash(self.__class__)
         for attr in ("service", "recordType", "shortNames", "guid",
-                     "enabledForCalendaring"):
+                     "enabled", "enabledForCalendaring"):
             h = (h + hash(getattr(self, attr))) & sys.maxint
 
         return h
 
+    def addAugmentInformation(self, augment):
+        
+        if augment:
+            self.enabled = augment.enabled
+            self.hostedAt = augment.hostedAt
+            self.enabledForCalendaring = augment.enabledForCalendaring
+            self.autoSchedule = augment.autoSchedule
+
+            if self.enabledForCalendaring and self.recordType == self.service.recordType_groups:
+                self.log_error("Group '%s(%s)' cannot be enabled for calendaring" % (self.guid, self.shortNames[0],))
+                self.enabledForCalendaring = False
+
+        else:
+            self.enabled = False
+            self.hostedAt = ""
+            self.enabledForCalendaring = False
+
     def members(self):
         return ()
 
@@ -396,6 +409,12 @@
                 return key
         return None
 
+    def locallyHosted(self):
+        return not self.hostedAt or not config.Partitioning.Enabled or self.hostedAt == config.Partitioning.ServerPartitionID
+    
+    def hostedURL(self):
+        return partitions.getPartitionURL(self.hostedAt)
+
 class DirectoryError(RuntimeError):
     """
     Generic directory error.

Modified: CalendarServer/trunk/twistedcaldav/directory/idirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/idirectory.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/idirectory.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -96,12 +96,15 @@
     recordType            = Attribute("The type of this record.")
     guid                  = Attribute("The GUID of this record.")
     uid                   = Attribute("The UID of this record.")
+    enabled               = Attribute("Determines whether this record should be provisioned as a principal.")
+    hostedAt              = Attribute("Identifies the server that actually hosts data for the record.")
     shortNames            = Attribute("The names for this record.")
     authIDs               = Attribute("Alternative security identities for this record.")
     fullName              = Attribute("The full name of this record.")
     firstName             = Attribute("The first name of this record.")
     lastName              = Attribute("The last name of this record.")
     emailAddress          = Attribute("The email address of this record.")
+    enabledForCalendaring = Attribute("Determines whether this record should be provisioned with a calendar home.")
     calendarUserAddresses = Attribute(
         """
         An iterable of C{str}s representing calendar user addresses for this
@@ -114,7 +117,6 @@
         hierarchy.
         """
     )
-    enabledForCalendaring = Attribute("Determines whether this record should be provisioned with a calendar home.")
 
     def members():
         """

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.directory.test.test_principal -*-
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -52,8 +52,7 @@
 from twistedcaldav.config import config
 from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
 
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
-from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
+from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyPrincipalResource
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from twistedcaldav.directory.util import NotFilePath
@@ -190,7 +189,7 @@
         raise NotImplementedError("Subclass must implement principalForCalendarUserAddress()")
 
     def principalForRecord(self, record):
-        if record is None:
+        if record is None or not record.enabled:
             return None
         return self.principalForUID(record.uid)
 
@@ -329,7 +328,7 @@
         else:
             # Next try looking it up in the directory
             record = self.directory.recordWithCalendarUserAddress(address)
-            if record is not None:
+            if record is not None and record.enabled:
                 return self.principalForRecord(record)
 
         log.debug("No principal for calendar user address: %r" % (address,))
@@ -410,8 +409,9 @@
 
             def _recordShortnameExpand():
                 for record in self.directory.listRecords(self.recordType):
-                    for shortName in record.shortNames:
-                        yield shortName
+                    if record.enabled:
+                        for shortName in record.shortNames:
+                            yield shortName
 
             return _recordShortnameExpand()
         else:
@@ -485,8 +485,7 @@
 
         record = self.directory.recordWithUID(primaryUID)
 
-        primaryPrincipal = self.principalForRecord(record)
-
+        primaryPrincipal = self.principalForRecord(record) if record and record.enabled else None
         if primaryPrincipal is None:
             log.err("No principal found for UID: %s" % (name,))
             return None
@@ -527,7 +526,7 @@
         """
         super(DirectoryPrincipalResource, self).__init__(NotFilePath(isdir=True))
 
-        self.cacheNotifier = self.cacheNotifierFactory(self)
+        self.cacheNotifier = self.cacheNotifierFactory(self, cacheHandle="PrincipalToken")
 
         if self.isCollection():
             slash = "/"
@@ -660,16 +659,11 @@
         """
         Return the SQL database for calendar user proxies.
 
-        @return: the L{CalendarUserProxyDatabase} for the principal collection.
+        @return: the L{ProxyDB} for the principal collection.
         """
 
-        # Get the principal collection we are contained in
-        pcollection = self.parent.parent
-
         # The db is located in the principal collection root
-        if not hasattr(pcollection, "calendar_user_proxy_db"):
-            setattr(pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(config.DataRoot))
-        return pcollection.calendar_user_proxy_db
+        return calendaruserproxy.ProxyDBService
 
     def alternateURIs(self):
         # FIXME: Add API to IDirectoryRecord for getting a record URI?
@@ -777,45 +771,22 @@
     def principalUID(self):
         return self.record.uid
 
+    def locallyHosted(self):
+        return self.record.locallyHosted()
+    
+    def hostedURL(self):
+        return self.record.hostedURL()
 
     ##
     # Extra resource info
     ##
 
     def setAutoSchedule(self, autoSchedule):
-        self._resource_info_index().setAutoSchedule(self.record.guid, autoSchedule)
+        self.record.autoSchedule = autoSchedule
 
-    @inlineCallbacks
     def getAutoSchedule(self):
-        value = (yield self._resource_info_index().getAutoSchedule(self.record.guid))
-        if value is None:
-            # No value has been explicitly set yet.  If this is a user/group
-            # the default should be False.  If resource/location, True.
-            if self.record.recordType in (DirectoryService.recordType_locations,
-                DirectoryService.recordType_resources):
-                yield self.setAutoSchedule(True)
-                returnValue(True)
-            else:
-                yield self.setAutoSchedule(False)
-                returnValue(False)
-        else:
-            returnValue(value)
+        return self.record.autoSchedule
 
-
-    def _resource_info_index(self):
-        """
-        Return the resource info SQL database for this calendar principal.
-
-        @return: the L{ResourceInfoDatabase} for the calendar principal.
-        """
-
-        # The db is located in the data root
-        self.pcollection = self.parent.parent
-        if not hasattr(self.pcollection, "resource_info_db"):
-            setattr(self.pcollection, "resource_info_db", ResourceInfoDatabase(config.DataRoot))
-        return self.pcollection.resource_info_db
-
-
     ##
     # Static
     ##

Deleted: CalendarServer/trunk/twistedcaldav/directory/sqldb.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/sqldb.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/sqldb.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,368 +0,0 @@
-##
-# Copyright (c) 2006-2009 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.
-##
-
-"""
-SQL (sqlite) based user/group/resource directory service implementation.
-"""
-
-"""
-SCHEMA:
-
-User Database:
-
-ROW: RECORD_TYPE, SHORT_NAME (unique), PASSWORD, NAME
-
-Group Database:
-
-ROW: SHORT_NAME, MEMBER_SHORT_NAME
-
-CUAddress database:
-
-ROW: ADDRESS (unqiue), SHORT_NAME
-
-"""
-
-__all__ = [
-    "SQLDirectoryService",
-]
-
-from twisted.cred.credentials import UsernamePassword
-from twisted.python.filepath import FilePath
-
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
-from twistedcaldav.sql import AbstractSQLDatabase
-from twistedcaldav.sql import db_prefix
-
-import os
-
-class SQLDirectoryManager(AbstractSQLDatabase):
-    """
-    House keeping operations on the SQL DB, including loading from XML file,
-    and record dumping. This can be used as a standalong DB management tool.
-    """
-    dbType = "DIRECTORYSERVICE"
-    dbFilename = db_prefix + "accounts"
-    dbFormatVersion = "3"
-
-    def __init__(self, path):
-        path = os.path.join(path, SQLDirectoryManager.dbFilename)
-        super(SQLDirectoryManager, self).__init__(path, True)
-
-    def loadFromXML(self, xmlFile):
-        parser = XMLAccountsParser(xmlFile)
-       
-        # Totally wipe existing DB and start from scratch
-        if os.path.exists(self.dbpath):
-            os.remove(self.dbpath)
-
-        self._db_execute("insert into SERVICE (REALM) values (:1)", parser.realm)
-
-        # Now add records to db
-        for item in parser.items.values():
-            for entry in item.itervalues():
-                self._add_to_db(entry)
-        self._db_commit()
-
-    def getRealm(self):
-        for realm in self._db_execute("select REALM from SERVICE"):
-            return realm[0].decode("utf-8")
-        else:
-            return ""
-
-    def listRecords(self, recordType):
-        # Get each account record
-        for shortName, guid, password, name in self._db_execute(
-            """
-            select SHORT_NAME, GUID, PASSWORD, NAME
-              from ACCOUNTS
-             where RECORD_TYPE = :1
-            """, recordType
-        ):
-            # See if we have members
-            members = self.members(shortName)
-                
-            # See if we are a member of any groups
-            groups = self.groups(shortName)
-                
-            # Get calendar user addresses
-            calendarUserAddresses = self.calendarUserAddresses(shortName)
-            
-            yield shortName, guid, password, name, members, groups, calendarUserAddresses
-
-    def getRecord(self, recordType, shortName):
-        # Get individual account record
-        for shortName, guid, password, name in self._db_execute(
-            """
-            select SHORT_NAME, GUID, PASSWORD, NAME
-              from ACCOUNTS
-             where RECORD_TYPE = :1
-               and SHORT_NAME  = :2
-            """, recordType, shortName
-        ):
-            break
-        else:
-            return None
-        
-        # See if we have members
-        members = self.members(shortName)
-            
-        # See if we are a member of any groups
-        groups = self.groups(shortName)
-            
-        # Get calendar user addresses
-        calendarUserAddresses = self.calendarUserAddresses(shortName)
-        
-        return shortName, guid, password, name, members, groups, calendarUserAddresses
-            
-    def members(self, shortName):
-        members = set()
-        for member in self._db_execute(
-            """
-            select MEMBER_RECORD_TYPE, MEMBER_SHORT_NAME
-              from GROUPS
-             where SHORT_NAME = :1
-            """, shortName
-        ):
-            members.add(tuple(member))
-        return members
-
-    def groups(self, shortName):
-        groups = set()
-        for (name,) in self._db_execute(
-            """
-            select SHORT_NAME
-              from GROUPS
-             where MEMBER_SHORT_NAME = :1
-            """, shortName
-        ):
-            groups.add(name)
-        return groups
-
-    def calendarUserAddresses(self, shortName):
-        calendarUserAddresses = set()
-        for (address,) in self._db_execute(
-            """
-            select ADDRESS
-              from ADDRESSES
-             where SHORT_NAME = :1
-            """, shortName
-        ):
-            calendarUserAddresses.add(address)
-        return calendarUserAddresses
-
-    def _add_to_db(self, record):
-        # Do regular account entry
-        recordType = record.recordType
-        shortName = record.shortNames[0]
-        guid = record.guid
-        password = record.password
-        name = record.fullName
-
-        self._db_execute(
-            """
-            insert into ACCOUNTS (RECORD_TYPE, SHORT_NAME, GUID, PASSWORD, NAME)
-            values (:1, :2, :3, :4, :5)
-            """, recordType, shortName, guid, password, name
-        )
-        
-        # Check for members
-        for memberRecordType, memberShortName in record.members:
-            self._db_execute(
-                """
-                insert into GROUPS (SHORT_NAME, MEMBER_RECORD_TYPE, MEMBER_SHORT_NAME)
-                values (:1, :2, :3)
-                """, shortName, memberRecordType, memberShortName
-            )
-                
-        # CUAddress
-        for cuaddr in record.calendarUserAddresses:
-            self._db_execute(
-                """
-                insert into ADDRESSES (ADDRESS, SHORT_NAME)
-                values (:1, :2)
-                """, cuaddr, shortName
-            )
-       
-    def _delete_from_db(self, shortName):
-        """
-        Deletes the specified entry from all dbs.
-        @param name: the name of the resource to delete.
-        @param shortName: the short name of the resource to delete.
-        """
-        self._db_execute("delete from ACCOUNTS  where SHORT_NAME        = :1", shortName)
-        self._db_execute("delete from GROUPS    where SHORT_NAME        = :1", shortName)
-        self._db_execute("delete from GROUPS    where MEMBER_SHORT_NAME = :1", shortName)
-        self._db_execute("delete from ADDRESSES where SHORT_NAME        = :1", shortName)
-    
-    def _db_version(self):
-        """
-        @return: the schema version assigned to this index.
-        """
-        return SQLDirectoryManager.dbFormatVersion
-        
-    def _db_type(self):
-        """
-        @return: the collection type assigned to this index.
-        """
-        return SQLDirectoryManager.dbType
-        
-    def _db_init_data_tables(self, q):
-        """
-        Initialise the underlying database tables.
-        @param q:           a database cursor to use.
-        """
-        #
-        # SERVICE table
-        #
-        q.execute("create table SERVICE (REALM text)")
-
-        #
-        # ACCOUNTS table
-        #
-        q.execute(
-            """
-            create table ACCOUNTS (
-                RECORD_TYPE  text,
-                SHORT_NAME   text,
-                GUID         text,
-                PASSWORD     text,
-                NAME         text
-            )
-            """
-        )
-
-        #
-        # GROUPS table
-        #
-        q.execute(
-            """
-            create table GROUPS (
-                SHORT_NAME          text,
-                MEMBER_RECORD_TYPE  text,
-                MEMBER_SHORT_NAME   text
-            )
-            """
-        )
-
-        #
-        # ADDRESSES table
-        #
-        q.execute(
-            """
-            create table ADDRESSES (
-                ADDRESS     text unique,
-                SHORT_NAME  text
-            )
-            """
-        )
-
-class SQLDirectoryService(DirectoryService):
-    """
-    XML based implementation of L{IDirectoryService}.
-    """
-    baseGUID = "8256E464-35E0-4DBB-A99C-F0E30C231675"
-    realmName = None
-
-    def __repr__(self):
-        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.manager.dbpath)
-
-    def __init__(self, dbParentPath, xmlFile=None):
-        super(SQLDirectoryService, self).__init__()
-
-        if type(dbParentPath) is str:
-            dbParentPath = FilePath(dbParentPath)
-            
-        self.manager = SQLDirectoryManager(dbParentPath.path)
-        if xmlFile:
-            self.manager.loadFromXML(xmlFile)
-        self.realmName = self.manager.getRealm()
-
-    def recordTypes(self):
-        recordTypes = (
-            DirectoryService.recordType_users,
-            DirectoryService.recordType_groups,
-            DirectoryService.recordType_locations,
-            DirectoryService.recordType_resources,
-        )
-        return recordTypes
-
-    def listRecords(self, recordType):
-        for result in self.manager.listRecords(recordType):
-            yield SQLDirectoryRecord(
-                service               = self,
-                recordType            = recordType,
-                shortName             = result[0],
-                guid                  = result[1],
-                password              = result[2],
-                name                  = result[3],
-                members               = result[4],
-                groups                = result[5],
-                calendarUserAddresses = result[6],
-            )
-
-    def recordWithShortName(self, recordType, shortName):
-        result = self.manager.getRecord(recordType, shortName)
-        if result:
-            return SQLDirectoryRecord(
-                service               = self,
-                recordType            = recordType,
-                shortName             = result[0],
-                guid                  = result[1],
-                password              = result[2],
-                name                  = result[3],
-                members               = result[4],
-                groups                = result[5],
-                calendarUserAddresses = result[6],
-            )
-
-        return None
-
-class SQLDirectoryRecord(DirectoryRecord):
-    """
-    XML based implementation implementation of L{IDirectoryRecord}.
-    """
-    def __init__(self, service, recordType, shortName, guid, password, name, members, groups, calendarUserAddresses):
-        super(SQLDirectoryRecord, self).__init__(
-            service               = service,
-            recordType            = recordType,
-            guid                  = guid,
-            shortNames            = (shortName,),
-            fullName              = name,
-            calendarUserAddresses = calendarUserAddresses,
-        )
-
-        self.password = password
-        self._members = members
-        self._groups  = groups
-
-    def members(self):
-        for recordType, shortName in self._members:
-            yield self.service.recordWithShortName(recordType, shortName)
-
-    def groups(self):
-        for shortName in self._groups:
-            yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
-
-    def verifyCredentials(self, credentials):
-        if isinstance(credentials, UsernamePassword):
-            return credentials.password == self.password
-
-        return super(SQLDirectoryRecord, self).verifyCredentials(credentials)
-
-if __name__ == '__main__':
-    mgr = SQLDirectoryManager("./")
-    mgr.loadFromXML("test/accounts.xml")

Modified: CalendarServer/trunk/twistedcaldav/directory/sudo.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/sudo.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/sudo.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -127,11 +127,12 @@
             guid=None,
             shortNames=(shortName,),
             fullName=shortName,
-            enabledForCalendaring=False,
         )
 
         self.password = entry['password']
 
+        self.enabled = True     # Explicitly enabled
+
     def verifyCredentials(self, credentials):
         if IUsernamePassword.providedBy(credentials):
             return credentials.checkPassword(self.password)

Modified: CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/accounts.xml	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+Copyright (c) 2006-2010 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.
@@ -16,7 +16,7 @@
 limitations under the License.
  -->
 
-<!DOCTYPE accounts SYSTEM "../../../conf/accounts.dtd">
+<!DOCTYPE accounts SYSTEM "../../../conf/auth/accounts.dtd">
 
 <accounts realm="Test">
   <user>
@@ -66,7 +66,6 @@
     <password>radnelacon</password>
     <name>No Calendar</name>
     <email-address>nocalendar at example.com</email-address>
-    <disable-calendar/>
   </user>
   <user repeat="2">
     <uid>user%02d</uid>
@@ -162,7 +161,6 @@
       <member>cdaboo</member>
       <member>lecroy</member>
     </members>
-    <disable-calendar/>
   </group>
   <location>
     <uid>mercury</uid>
@@ -170,9 +168,6 @@
     <password>mercury</password>
     <name>Mecury Seven</name>
     <email-address>mercury at example.com</email-address>
-    <proxies>
-      <member type="groups">left_coast</member>
-    </proxies>
   </location>
   <location>
     <uid>gemini</uid>
@@ -180,10 +175,6 @@
     <password>gemini</password>
     <name>Gemini Twelve</name>
     <email-address>gemini at example.com</email-address>
-    <auto-schedule/>
-    <proxies>
-      <member>wsanchez</member>
-    </proxies>
   </location>
   <location>
     <uid>apollo</uid>
@@ -191,9 +182,6 @@
     <password>apollo</password>
     <name>Apollo Eleven</name>
     <email-address>apollo at example.com</email-address>
-    <proxies>
-      <member type="groups">both_coasts</member>
-    </proxies>
   </location>
   <location>
     <uid>orion</uid>
@@ -201,9 +189,6 @@
     <password>orion</password>
     <name>Orion</name>
     <email-address>orion at example.com</email-address>
-    <proxies>
-      <member type="groups">recursive1_coasts</member>
-    </proxies>
   </location>
   <resource>
     <uid>transporter</uid>
@@ -225,11 +210,5 @@
     <password>non_calendar_proxy</password>
     <name>Non-calendar proxy</name>
     <email-address>non_calendar_proxy at example.com</email-address>
-    <proxies>
-      <member type="groups">non_calendar_group</member>
-    </proxies>
-    <read-only-proxies>
-      <member type="groups">recursive2_coasts</member>
-    </read-only-proxies>
   </resource>
 </accounts>

Copied: CalendarServer/trunk/twistedcaldav/directory/test/augments-test-default.xml (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/directory/test/augments-test-default.xml)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/augments-test-default.xml	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/test/augments-test-default.xml	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2009-2010 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 "../../../conf/auth/augments.dtd">
+
+<augments>
+  <record>
+    <guid>Default</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <hosted-at>00001</hosted-at>
+  </record>
+  <record>
+    <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+    <enable>false</enable>
+  </record>
+  <record>
+    <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+    <enable>true</enable>
+    <enable-calendar>false</enable-calendar>
+  </record>
+  <record>
+    <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <enable>true</enable>
+    <hosted-at>00001</hosted-at>
+  </record>
+  <record>
+    <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+    <enable>true</enable>
+    <hosted-at>00002</hosted-at>
+  </record>
+  <record>
+    <guid>6A73326A-F781-47E7-A9F8-AF47364D4152</guid>
+    <enable>true</enable>
+    <hosted-at>00002</hosted-at>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+</augments>

Copied: CalendarServer/trunk/twistedcaldav/directory/test/augments-test.xml (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/directory/test/augments-test.xml)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/augments-test.xml	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/test/augments-test.xml	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2009-2010 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 "../../../conf/auth/augments.dtd">
+
+<augments>
+  <record>
+    <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+    <enable>false</enable>
+  </record>
+  <record>
+    <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+    <enable>true</enable>
+    <enable-calendar>false</enable-calendar>
+  </record>
+  <record>
+    <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <enable>true</enable>
+    <hosted-at>00001</hosted-at>
+  </record>
+  <record>
+    <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+    <enable>true</enable>
+    <hosted-at>00002</hosted-at>
+  </record>
+  <record>
+    <guid>6A73326A-F781-47E7-A9F8-AF47364D4152</guid>
+    <enable>true</enable>
+    <hosted-at>00002</hosted-at>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+</augments>

Copied: CalendarServer/trunk/twistedcaldav/directory/test/augments.xml (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/directory/test/augments.xml)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/augments.xml	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/test/augments.xml	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2009-2010 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 "../../../conf/auth/augments.dtd">
+
+<augments realm="Test">
+  <record>
+    <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+    <enable>true</enable>
+    <enable-calendar>false</enable-calendar>
+  </record>
+  <record repeat="2">
+    <guid>user%02d</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>admin</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>grunts</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>right_coast</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>left_coast</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>both_coasts</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>recursive1_coasts</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>recursive2_coasts</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>non_calendar_group</guid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <guid>mercury</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>gemini</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>apollo</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>orion</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>transporter</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>ftlcpu</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+  <record>
+    <guid>non_calendar_proxy</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+  </record>
+</augments>

Deleted: CalendarServer/trunk/twistedcaldav/directory/test/basic
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/basic	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/basic	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,4 +0,0 @@
-wsanchez:Cytm0Bwm7CPJs
-cdaboo:I.Ef5FJl5GVh2
-dreid:LVhqAv4qSrYPs
-lecroy:/7/5VDrkrLxY.

Deleted: CalendarServer/trunk/twistedcaldav/directory/test/digest
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/digest	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/digest	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,4 +0,0 @@
-wsanchez:Test:decbe233ab3d997cacc2fc058b19db8c
-cdaboo:Test:61164bf3d607d072fe8a7ac420b24aac
-dreid:Test:8ee67801004b2752f72b84e7064889a6
-lecroy:Test:60d4feb424430953be045738041e51be

Deleted: CalendarServer/trunk/twistedcaldav/directory/test/groups
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/groups	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/groups	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,4 +0,0 @@
-managers: lecroy
-grunts: wsanchez, cdaboo, dreid
-right_coast: cdaboo
-left_coast: wsanchez, dreid, lecroy

Copied: CalendarServer/trunk/twistedcaldav/directory/test/proxies.xml (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/directory/test/proxies.xml)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/proxies.xml	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/test/proxies.xml	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2010 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 proxies SYSTEM "proxies.dtd">
+
+<proxies>
+  <record>
+    <guid>mercury</guid>
+    <proxies>
+      <member>left_coast</member>
+    </proxies>
+  </record>
+  <record>
+    <guid>gemini</guid>
+    <proxies>
+      <member>6423F94A-6B76-4A3A-815B-D52CFD77935D</member>
+    </proxies>
+  </record>
+  <record>
+    <guid>apollo</guid>
+    <proxies>
+      <member>both_coasts</member>
+    </proxies>
+  </record>
+  <record>
+    <guid>orion</guid>
+    <proxies>
+      <member>recursive1_coasts</member>
+    </proxies>
+  </record>
+  <record>
+    <guid>non_calendar_proxy</guid>
+    <proxies>
+      <member>non_calendar_group</member>
+    </proxies>
+    <read-only-proxies>
+      <member>recursive2_coasts</member>
+    </read-only-proxies>
+  </record>
+</proxies>

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_aggregate.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_aggregate.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_aggregate.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -14,20 +14,18 @@
 # limitations under the License.
 ##
 
-from twistedcaldav.directory.apache import BasicDirectoryService
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 
-from twistedcaldav.directory.test.test_apache import digestRealm, basicUserFile, groupFile
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
 
 import twistedcaldav.directory.test.util
+from twistedcaldav.directory import augment
 
 apache_prefix = "apache:"
 xml_prefix = "xml:"
 
 testServices = (
-    (apache_prefix, twistedcaldav.directory.test.test_apache.Apache  ),
     (xml_prefix   , twistedcaldav.directory.test.test_xmlfile.XMLFile),
 )
 
@@ -65,16 +63,9 @@
         """
         Returns an IDirectoryService.
         """
-        apacheService = BasicDirectoryService(
-            {
-                'realmName' : digestRealm,
-                'userFile' : basicUserFile,
-                'groupFile' : groupFile,
-            }
-        )
-        apacheService.recordTypePrefix = apache_prefix
-
         xmlService = XMLDirectoryService({'xmlFile' : xmlFile})
         xmlService.recordTypePrefix = xml_prefix
 
-        return AggregateDirectoryService((apacheService, xmlService))
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+
+        return AggregateDirectoryService((xmlService,))

Deleted: CalendarServer/trunk/twistedcaldav/directory/test/test_apache.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_apache.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_apache.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,128 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-##
-
-import os
-
-from twisted.python.filepath import FilePath
-
-import twistedcaldav.directory.test.util
-from twistedcaldav.directory.apache import BasicDirectoryService, DigestDirectoryService
-from twistedcaldav.directory.directory import DirectoryService
-
-digestRealm = "Test"
-
-basicUserFile  = FilePath(os.path.join(os.path.dirname(__file__), "basic"))
-digestUserFile = FilePath(os.path.join(os.path.dirname(__file__), "digest"))
-groupFile      = FilePath(os.path.join(os.path.dirname(__file__), "groups"))
-
-# FIXME: Add tests for GUID hooey, once we figure out what that means here
-
-class Apache (object):
-    recordTypes = set((
-        DirectoryService.recordType_users,
-        DirectoryService.recordType_groups
-    ))
-
-    users = {
-        "wsanchez": { "password": "foo",  "guid": None, "addresses": () },
-        "cdaboo"  : { "password": "bar",  "guid": None, "addresses": () },
-        "dreid"   : { "password": "baz",  "guid": None, "addresses": () },
-        "lecroy"  : { "password": "quux", "guid": None, "addresses": () },
-    }
-
-    groups = {
-        "managers"   : { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "lecroy"),)                                         },
-        "grunts"     : { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "wsanchez"),
-                                                                    (DirectoryService.recordType_users, "cdaboo"),
-                                                                    (DirectoryService.recordType_users, "dreid")) },
-        "right_coast": { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "cdaboo"),)                                         },
-        "left_coast" : { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "wsanchez"),
-                                                                    (DirectoryService.recordType_users, "dreid"),
-                                                                    (DirectoryService.recordType_users, "lecroy")) },
-    }
-
-    locations = {
-    }
-
-    resources = {
-    }
-
-    def service(self):
-        return self.serviceClass(
-            {
-                'realmName' : digestRealm,
-                'userFile' : self.userFile(),
-                'groupFile' : self.groupFile(),
-            }
-        )
-
-    userFileName = None
-
-    def userFile(self):
-        if not hasattr(self, "_userFile"):
-            if self.userFileName is None:
-                raise NotImplementedError("Test subclass needs to specify userFileName.")
-            self._userFile = FilePath(self.mktemp())
-            basicUserFile.copyTo(self._userFile)
-        return self._userFile
-
-    def groupFile(self):
-        if not hasattr(self, "_groupFile"):
-            self._groupFile = FilePath(self.mktemp())
-            groupFile.copyTo(self._groupFile)
-        return self._groupFile
-
-    def test_changedGroupFile(self):
-        self.groupFile().open("w").write("grunts: wsanchez\n")
-        self.assertEquals(self.recordNames(DirectoryService.recordType_groups), set(("grunts",)))
-
-    def test_recordTypes_user(self):
-        """
-        IDirectoryService.recordTypes(userFile)
-        """
-        self.assertEquals(set(self.serviceClass({'realmName':digestRealm, 'userFile':self.userFile()}).recordTypes()), set((DirectoryService.recordType_users,)))
-
-    userEntry = None
-
-    def test_changedUserFile(self):
-        if self.userEntry is None:
-            raise NotImplementedError("Test subclass needs to specify userEntry.")
-        self.userFile().open("w").write(self.userEntry[1])
-        self.assertEquals(self.recordNames(DirectoryService.recordType_users), set((self.userEntry[0],)))
-
-class Basic (Apache, twistedcaldav.directory.test.util.BasicTestCase,
-    twistedcaldav.directory.test.util.NonCachingTestCase):
-    """
-    Test Apache-Compatible UserFile/GroupFile directory implementation.
-    """
-    serviceClass = BasicDirectoryService
-
-    userFileName = basicUserFile
-    userEntry = ("wsanchez", "wsanchez:Cytm0Bwm7CPJs\n")
-
-class Digest (Apache, twistedcaldav.directory.test.util.DigestTestCase,
-    twistedcaldav.directory.test.util.NonCachingTestCase):
-    """
-    Test Apache-Compatible DigestFile/GroupFile directory implementation.
-    """
-    serviceClass = DigestDirectoryService
-
-    userFileName = digestUserFile
-    userEntry = ("wsanchez", "wsanchez:Test:decbe233ab3d997cacc2fc058b19db8c\n")
-
-    def test_verifyCredentials_digest(self):
-        raise NotImplementedError() # Use super's implementation
-    test_verifyCredentials_digest.todo = "unimplemented"

Copied: CalendarServer/trunk/twistedcaldav/directory/test/test_augment.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/directory/test/test_augment.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_augment.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_augment.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,178 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.directory.augment import AugmentXMLDB, AugmentSqliteDB,\
+    AugmentPostgreSQLDB
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.directory.xmlaugmentsparser import XMLAugmentsParser
+import cStringIO
+import os
+
+xmlFile = os.path.join(os.path.dirname(__file__), "augments-test.xml")
+xmlFileDefault = os.path.join(os.path.dirname(__file__), "augments-test-default.xml")
+
+testRecords = (
+    {"guid":"D11F03A0-97EA-48AF-9A6C-FAC7F3975766", "enabled":True,  "hostedAt":"", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"6423F94A-6B76-4A3A-815B-D52CFD77935D", "enabled":True,  "hostedAt":"", "enabledForCalendaring":True, "autoSchedule":False},
+    {"guid":"5A985493-EE2C-4665-94CF-4DFEA3A89500", "enabled":False, "hostedAt":"", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"8B4288F6-CC82-491D-8EF9-642EF4F3E7D0", "enabled":True,  "hostedAt":"", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "enabled":True,  "hostedAt":"00001", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"543D28BA-F74F-4D5F-9243-B3E3A61171E5", "enabled":True,  "hostedAt":"00002", "enabledForCalendaring":False, "autoSchedule":False},
+    {"guid":"6A73326A-F781-47E7-A9F8-AF47364D4152", "enabled":True,  "hostedAt":"00002", "enabledForCalendaring":True, "autoSchedule":True},
+)
+
+testRecordDefault = {"guid":"A4318887-F2C7-4A70-9056-B88CC8DB26F1", "enabled":True,  "hostedAt":"00001", "enabledForCalendaring":True, "autoSchedule":False}
+
+class AugmentTests(TestCase):
+
+    @inlineCallbacks
+    def _checkRecord(self, db, items):
+        
+        record = (yield db.getAugmentRecord(items["guid"]))
+        self.assertTrue(record is not None)
+        
+        for k,v in items.iteritems():
+            self.assertEqual(getattr(record, k), v)
+
+    @inlineCallbacks
+    def _checkNoRecord(self, db, guid):
+        
+        record = (yield db.getAugmentRecord(guid))
+        self.assertTrue(record is None)
+
+class AugmentXMLTests(AugmentTests):
+
+    @inlineCallbacks
+    def test_read(self):
+        
+        db = AugmentXMLDB((xmlFile,))
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkNoRecord(db, "D11F03A0-97EA-48AF-9A6C-FAC7F3975767")
+
+    @inlineCallbacks
+    def test_read_default(self):
+        
+        db = AugmentXMLDB((xmlFileDefault,))
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkRecord(db, testRecordDefault)
+
+    def test_parseErrors(self):
+        
+        db = {}
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO(""), db)
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+<accounts>
+    <foo/>
+</accounts>
+"""), db)
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+<augments>
+    <foo/>
+</augments>
+"""), db)
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+<augments>
+  <record>
+    <enable>true</enable>
+  </record>
+</augments>
+"""), db)
+        self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+  <record>
+    <guid>admin</guid>
+    <enable>true</enable>
+    <foo/>
+  </record>
+"""), db)
+
+class AugmentSqliteTests(AugmentTests):
+
+    @inlineCallbacks
+    def test_read(self):
+        
+        db = AugmentSqliteDB(self.mktemp())
+
+        dbxml = AugmentXMLDB((xmlFile,))
+        for record in dbxml.db.values():
+            yield db.addAugmentRecord(record)
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkNoRecord(db, "D11F03A0-97EA-48AF-9A6C-FAC7F3975767")
+
+    @inlineCallbacks
+    def test_read_default(self):
+        
+        db = AugmentSqliteDB(self.mktemp())
+
+        dbxml = AugmentXMLDB((xmlFileDefault,))
+        for record in dbxml.db.values():
+            yield db.addAugmentRecord(record)
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkRecord(db, testRecordDefault)
+
+class AugmentPostgreSQLTests(AugmentTests):
+
+    @inlineCallbacks
+    def test_read(self):
+        
+        db = AugmentPostgreSQLDB("localhost", "augments")
+        yield db.clean()
+
+        dbxml = AugmentXMLDB((xmlFile,))
+        for record in dbxml.db.values():
+            yield db.addAugmentRecord(record)
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkNoRecord(db, "D11F03A0-97EA-48AF-9A6C-FAC7F3975767")
+
+    @inlineCallbacks
+    def test_read_default(self):
+        
+        db = AugmentPostgreSQLDB("localhost", "augments")
+        yield db.clean()
+
+        dbxml = AugmentXMLDB((xmlFileDefault,))
+        for record in dbxml.db.values():
+            yield db.addAugmentRecord(record)
+
+        for item in testRecords:
+            yield self._checkRecord(db, item)
+
+        yield self._checkRecord(db, testRecordDefault)
+
+try:
+    import pgdb
+except ImportError:
+    AugmentPostgreSQLTests.skip = True
+else:
+    try:
+        db = pgdb.connect(host="localhost", database="augments")
+    except:
+        AugmentPostgreSQLTests.skip = True

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_cachedirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_cachedirectory.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_cachedirectory.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2010 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.
@@ -20,6 +20,7 @@
 from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.directory.util import uuidFromName
 from uuid import uuid4
+from twistedcaldav.directory.augment import AugmentRecord
 
 class TestDirectoryService (CachingDirectoryService):
 
@@ -62,8 +63,16 @@
                         firstName             = "",
                         lastName              = "",
                         emailAddresses        = record.get("email"),
+                    ) 
+                    
+                    augmentRecord = AugmentRecord(
+                        guid = cacheRecord.guid,
+                        enabled=True,
                         enabledForCalendaring = True,
-                    ) 
+                    )
+                    
+                    cacheRecord.addAugmentInformation(augmentRecord)
+
                     self.recordCacheForType(recordType).addRecord(cacheRecord,
                         indexType, indexKey)
 

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_calendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_calendar.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_calendar.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -21,8 +21,9 @@
 from twisted.web2.test.test_server import SimpleRequest
 
 from twistedcaldav import caldavxml
+from twistedcaldav.directory import augment
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.static import CalendarHomeProvisioningFile
 
@@ -36,11 +37,12 @@
         super(ProvisionedCalendars, self).setUp()
         
         # Setup the initial directory
-        self.xmlfile = self.mktemp()
-        fd = open(self.xmlfile, "w")
+        self.xmlFile = self.mktemp()
+        fd = open(self.xmlFile, "w")
         fd.write(open(xmlFile.path, "r").read())
         fd.close()
-        self.directoryService = XMLDirectoryService({'xmlFile' : self.xmlfile})
+        self.directoryService = XMLDirectoryService({'xmlFile' : self.xmlFile})
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
         
         # Set up a principals hierarchy for each service we're testing with
         name = "principals"

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_guidchange.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_guidchange.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_guidchange.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -37,11 +37,11 @@
         super(ProvisionedPrincipals, self).setUp()
         
         # Setup the initial directory
-        self.xmlfile = self.mktemp()
-        fd = open(self.xmlfile, "w")
+        self.xmlFile = self.mktemp()
+        fd = open(self.xmlFile, "w")
         fd.write(open(xmlFile.path, "r").read())
         fd.close()
-        self.directoryService = XMLDirectoryService({'xmlFile' : self.xmlfile})
+        self.directoryService = XMLDirectoryService({'xmlFile' : self.xmlFile})
         
         # Set up a principals hierarchy for each service we're testing with
         name = "principals"
@@ -78,7 +78,7 @@
         
         def privs1(result):
             # Change GUID in record
-            fd = open(self.xmlfile, "w")
+            fd = open(self.xmlFile, "w")
             fd.write(open(xmlFile.path, "r").read().replace(oldUID, newUID))
             fd.close()
             fd = None

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -21,6 +21,7 @@
 else:
     import twisted.web2.auth.digest
     import twistedcaldav.directory.test.util
+    from twistedcaldav.directory import augment
     from twisted.internet.defer import inlineCallbacks
     from twistedcaldav.directory.directory import DirectoryService
     from twistedcaldav.directory.appleopendirectory import OpenDirectoryRecord
@@ -53,6 +54,7 @@
         def setUp(self):
             super(OpenDirectory, self).setUp()
             self._service = OpenDirectoryService({'node' : "/Search"}, dosetup=False)
+            augment.AugmentService = augment.AugmentXMLDB(xmlFiles=())
 
         def tearDown(self):
             for call in self._service._delayedCalls:
@@ -73,7 +75,6 @@
                 firstName             = "Some",
                 lastName              = "User",
                 emailAddresses        = set(("someuser at example.com",)),
-                enabledForCalendaring = True,
                 memberGUIDs           = [],
             )
             self.assertEquals(record.fullName, "")
@@ -90,7 +91,6 @@
                 firstName             = "Some",
                 lastName              = "User",
                 emailAddresses        = set(("someuser at example.com",)),
-                enabledForCalendaring = True,
                 memberGUIDs           = [],
             )
 
@@ -111,7 +111,6 @@
                 firstName             = "Some",
                 lastName              = "User",
                 emailAddresses        = set(("someuser at example.com",)),
-                enabledForCalendaring = True,
                 memberGUIDs           = [],
             )
 

Deleted: CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryschema.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryschema.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryschema.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,147 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-##
-
-try:
-    from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
-except ImportError:
-    pass
-else:
-    from twistedcaldav.test.util import TestCase
-
-    class ODResourceInfoParse (TestCase):
-
-        plist_good_false = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <dict>
-        <key>AutoAcceptsInvitation</key>
-        <false/>
-        <key>Label</key>
-        <string>Location</string>
-        <key>CalendaringDelegate</key>
-        <string>1234-GUID-5678</string>
-        <key>ReadOnlyCalendaringDelegate</key>
-        <string>1234-GUID-5679</string>
-    </dict>
-</dict>
-</plist>
-"""
-
-        plist_good_true = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <dict>
-        <key>AutoAcceptsInvitation</key>
-        <true/>
-        <key>Label</key>
-        <string>Location</string>
-        <key>CalendaringDelegate</key>
-        <string></string>
-        <key>ReadOnlyCalendaringDelegate</key>
-        <string></string>
-    </dict>
-</dict>
-</plist>
-"""
-
-        plist_good_missing = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <dict>
-        <key>Label</key>
-        <string>Location</string>
-    </dict>
-</dict>
-</plist>
-"""
-
-        plist_bad = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <string>bogus</string>
-</dict>
-</plist>
-"""
-
-        plist_wrong = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.YellowPagesFramework</key>
-    <dict>
-        <key>AutoAcceptsInvitation</key>
-        <true/>
-        <key>Label</key>
-        <string>Location</string>
-        <key>CalendaringDelegate</key>
-        <string>1234-GUID-5678</string>
-        <key>ReadOnlyCalendaringDelegate</key>
-        <string>1234-GUID-5679</string>
-    </dict>
-</dict>
-</plist>
-"""
-
-        plist_invalid = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <string>bogus</string>
-    <string>another bogon</string>
-</dict>
-</plist>
-"""
-
-        plist_invalid_xml = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-    <key>com.apple.WhitePagesFramework</key>
-    <string>R&D</string>
-</dict>
-</plist>
-"""
-
-        test_bool = (
-            (plist_good_false, False, "1234-GUID-5678", "1234-GUID-5679", None),
-            (plist_good_true, True, "", "", None),
-            (plist_good_missing, False, None, None, None),
-            (plist_wrong, False, None, None, None),
-            (plist_bad, False, None, None, ValueError),
-            (plist_invalid, False, None, None, ValueError),
-            (plist_invalid_xml, False, None, None, ValueError),
-        )
-
-        def test_plists(self):
-            service = OpenDirectoryService({'node' : "/Search"}, dosetup=False)
-            
-            for item in ODResourceInfoParse.test_bool:
-                if item[4] is None:
-                    item1, item2, item3 = service._parseResourceInfo(item[0], "guid", "locations", "name")
-                    self.assertEqual(item1, item[1])
-                    self.assertEqual(item2, item[2])
-                    self.assertEqual(item3, item[3])
-                else:
-                    self.assertRaises(item[4], service._parseResourceInfo, item[0], "guid", "locations", "name")

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_principal.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -25,11 +25,10 @@
 
 from twistedcaldav.static import CalendarHomeProvisioningFile
 from twistedcaldav.config import config
-from twistedcaldav.directory.apache import BasicDirectoryService, DigestDirectoryService
+from twistedcaldav.directory import augment, calendaruserproxy
 from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.test.test_apache import basicUserFile, digestUserFile, groupFile, digestRealm
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 from twistedcaldav.directory.principal import DirectoryPrincipalTypeProvisioningResource
 from twistedcaldav.directory.principal import DirectoryPrincipalResource
@@ -48,20 +47,6 @@
         super(ProvisionedPrincipals, self).setUp()
 
         self.directoryServices = (
-            BasicDirectoryService(
-                {
-                    'realmName' : digestRealm,
-                    'userFile' : basicUserFile,
-                    'groupFile' : groupFile,
-                }
-            ),
-            DigestDirectoryService(
-                {
-                    'realmName' : digestRealm,
-                    'userFile' : digestUserFile,
-                    'groupFile' : groupFile,
-                }
-            ),
             XMLDirectoryService(
                 {
                     'xmlFile' : xmlFile,
@@ -81,6 +66,9 @@
 
             self.principalRootResources[directory.__class__.__name__] = provisioningResource
 
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+        calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB(self.mktemp())
+
     def test_hierarchy(self):
         """
         DirectoryPrincipalProvisioningResource.listChildren(),
@@ -206,7 +194,7 @@
         """
         for provisioningResource, recordType, recordResource, record in self._allRecords():
             principal = provisioningResource.principalForRecord(record)
-            self.failIf(principal is None)
+            self.failIf(principal is None, msg=str(record))
             self.assertEquals(record, principal.record)
 
     def test_principalForCalendarUserAddress(self):
@@ -229,6 +217,7 @@
                     self.failIf(principal is not None)
 
         # Explicitly check the disabled record
+        provisioningResource = self.principalRootResources['XMLDirectoryService']
         self.failIf(provisioningResource.principalForCalendarUserAddress("mailto:nocalendar at example.com") is not None)
         self.failIf(provisioningResource.principalForCalendarUserAddress("urn:uuid:543D28BA-F74F-4D5F-9243-B3E3A61171E5") is not None)
         self.failIf(provisioningResource.principalForCalendarUserAddress("/principals/users/nocalendar/") is not None)
@@ -434,7 +423,7 @@
             def qname(self):
                 return self.ns, self.name
 
-        provisioningResource = self.principalRootResources['BasicDirectoryService']
+        provisioningResource = self.principalRootResources['XMLDirectoryService']
 
         expected = (
             ("DAV:", "displayname", "morgen", "fullName", "morgen"),

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipaldb.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipaldb.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipaldb.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -15,18 +15,19 @@
 ##
 from twistedcaldav.config import config
 
-import os
-
 from twisted.internet.defer import inlineCallbacks
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
+from twistedcaldav.directory.calendaruserproxy import ProxySqliteDB,\
+    ProxyPostgreSQLDB
 import twistedcaldav.test.util
+from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
+from twistedcaldav.directory import calendaruserproxy
 
-class ProxyPrincipalDB (twistedcaldav.test.util.TestCase):
+class ProxyPrincipalDBSqlite (twistedcaldav.test.util.TestCase):
     """
     Directory service provisioned principals.
     """
     
-    class old_CalendarUserProxyDatabase(CalendarUserProxyDatabase):
+    class old_ProxyDB(ProxySqliteDB):
         
         def _db_version(self):
             """
@@ -34,7 +35,7 @@
             """
             return "3"
             
-        def _db_init_data_tables(self, q):
+        def _db_init_data_tables(self):
             """
             Initialise the underlying database tables.
             @param q:           a database cursor to use.
@@ -43,7 +44,7 @@
             #
             # GROUPS table
             #
-            q.execute(
+            return self.execute(
                 """
                 create table GROUPS (
                     GROUPNAME   text,
@@ -52,7 +53,7 @@
                 """
             )
 
-    class new_CalendarUserProxyDatabase(CalendarUserProxyDatabase):
+    class new_ProxyDB(ProxySqliteDB):
         
         def _db_version(self):
             """
@@ -60,7 +61,7 @@
             """
             return "11"
             
-    class newer_CalendarUserProxyDatabase(CalendarUserProxyDatabase):
+    class newer_ProxyDB(ProxySqliteDB):
         
         def _db_version(self):
             """
@@ -73,8 +74,7 @@
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = CalendarUserProxyDatabase(db_path)
+        db = ProxySqliteDB(db_path)
         yield db.setGroupMembers("A", ("B", "C", "D",))
         
         membersA = yield db.getMembers("A")
@@ -83,29 +83,28 @@
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
 
+    @inlineCallbacks
     def test_DBIndexed(self):
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = CalendarUserProxyDatabase(db_path)
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
+        db = ProxySqliteDB(db_path)
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
 
+    @inlineCallbacks
     def test_OldDB(self):
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = self.old_CalendarUserProxyDatabase(db_path)
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set())
+        db = self.old_ProxyDB(db_path)
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set())
 
     @inlineCallbacks
     def test_DBUpgrade(self):
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = self.old_CalendarUserProxyDatabase(db_path)
+        db = self.old_ProxyDB(db_path)
         yield db.setGroupMembers("A", ("B", "C", "D",))
 
         membersA = yield db.getMembers("A")
@@ -113,19 +112,19 @@
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set())
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set())
+        db.close()
         db = None
         
-        db = CalendarUserProxyDatabase(db_path)
+        db = ProxySqliteDB(db_path)
 
         membersA = yield db.getMembers("A")
         membershipsB = yield db.getMemberships("B")
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+        db.close()
         db = None
 
     @inlineCallbacks
@@ -133,8 +132,7 @@
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = self.old_CalendarUserProxyDatabase(db_path)
+        db = self.old_ProxyDB(db_path)
         yield db.setGroupMembers("A", ("B", "C", "D",))
 
         membersA = yield db.getMembers("A")
@@ -142,19 +140,19 @@
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set())
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set())
+        db.close()
         db = None
         
-        db = self.new_CalendarUserProxyDatabase(db_path)
+        db = self.new_ProxyDB(db_path)
 
         membersA = yield db.getMembers("A")
         membershipsB = yield db.getMemberships("B")
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+        db.close()
         db = None
 
     @inlineCallbacks
@@ -162,8 +160,7 @@
     
         # Get the DB
         db_path = self.mktemp()
-        os.mkdir(db_path)
-        db = self.new_CalendarUserProxyDatabase(db_path)
+        db = self.new_ProxyDB(db_path)
         yield db.setGroupMembers("A", ("B", "C", "D",))
 
         membersA = yield db.getMembers("A")
@@ -171,19 +168,19 @@
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+        db.close()
         db = None
         
-        db = self.newer_CalendarUserProxyDatabase(db_path)
+        db = self.newer_ProxyDB(db_path)
 
         membersA = yield db.getMembers("A")
         membershipsB = yield db.getMemberships("B")
 
         self.assertEqual(membersA, set(("B", "C", "D",)))
         self.assertEqual(membershipsB, set(("A",)))
-        self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
-        db._db_close()
+        self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+        db.close()
         db = None
 
     @inlineCallbacks
@@ -194,8 +191,7 @@
 
             # Get the DB
             db_path = self.mktemp()
-            os.mkdir(db_path)
-            db = CalendarUserProxyDatabase(db_path)
+            db = ProxySqliteDB(db_path)
             
             # Do one insert and check the result
             yield db.setGroupMembers("A", ("B", "C", "D",))
@@ -235,8 +231,7 @@
 
             # Get the DB
             db_path = self.mktemp()
-            os.mkdir(db_path)
-            db = CalendarUserProxyDatabase(db_path)
+            db = ProxySqliteDB(db_path)
             
             # Do one insert and check the result
             yield db.setGroupMembers("A", ("B", "C", "D",))
@@ -277,8 +272,7 @@
 
             # Get the DB
             db_path = self.mktemp()
-            os.mkdir(db_path)
-            db = CalendarUserProxyDatabase(db_path)
+            db = ProxySqliteDB(db_path)
             
             # Do one insert and check the result
             yield db.setGroupMembers("A", ("B", "C", "D",))
@@ -311,8 +305,7 @@
 
             # Get the DB
             db_path = self.mktemp()
-            os.mkdir(db_path)
-            db = CalendarUserProxyDatabase(db_path)
+            db = ProxySqliteDB(db_path)
             
             # Do one insert and check the result
             yield db.setGroupMembers("A", ("B", "C", "D",))
@@ -353,8 +346,7 @@
 
             # Get the DB
             db_path = self.mktemp()
-            os.mkdir(db_path)
-            db = CalendarUserProxyDatabase(db_path)
+            db = ProxySqliteDB(db_path)
             
             # Do one insert and check the result for the one we will remove
             yield db.setGroupMembers("AA", ("BB", "CC", "DD",))
@@ -375,3 +367,234 @@
             self.assertEqual(membershipsDD, set())
             self.assertEqual(membershipsEE, set(("AA",)))
 
+class ProxyPrincipalDBPostgreSQL (twistedcaldav.test.util.TestCase):
+    """
+    Directory service provisioned principals.
+    """
+    
+    @inlineCallbacks
+    def setUp(self):
+
+        super(ProxyPrincipalDBPostgreSQL, self).setUp()
+        self.db = ProxyPostgreSQLDB(host="localhost", database="proxies")
+        yield self.db.clean()
+
+    @inlineCallbacks
+    def tearDown(self):
+        yield self.db.close()
+        self.db = None
+
+    @inlineCallbacks
+    def test_normalDB(self):
+    
+        # Get the DB
+        yield self.db.clean()
+        
+        calendaruserproxy.ProxyDBService = self.db
+        loader = XMLCalendarUserProxyLoader("/Volumes/Data/Users/cyrusdaboo/Documents/Development/Apple/eclipse/CalendarServer-3/conf/auth/proxies-test.xml")
+        yield loader.updateProxyDB()
+
+        yield self.db.setGroupMembers("A", ("B", "C", "D",))
+        
+        membersA = yield self.db.getMembers("A")
+        membershipsB = yield self.db.getMemberships("B")
+        
+        self.assertEqual(membersA, set(("B", "C", "D",)))
+        self.assertEqual(membershipsB, set(("A",)))
+
+    @inlineCallbacks
+    def test_DBIndexed(self):
+    
+        # Get the DB
+        yield self.db.clean()
+        self.assertTrue((yield self.db.queryOne("select hasindexes from pg_tables where tablename = 'groups'")))
+
+    @inlineCallbacks
+    def test_cachingDBInsert(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "D",))
+    
+            membersA = yield self.db.getMembers("A")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+            membershipsE = yield self.db.getMemberships("E")
+    
+            self.assertEqual(membersA, set(("B", "C", "D",)))
+            self.assertEqual(membershipsB, set(("A",)))
+            self.assertEqual(membershipsC, set(("A",)))
+            self.assertEqual(membershipsD, set(("A",)))
+            self.assertEqual(membershipsE, set(()))
+            
+            # Change and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "E",))
+    
+            membersA = yield self.db.getMembers("A")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+            membershipsE = yield self.db.getMemberships("E")
+    
+            self.assertEqual(membersA, set(("B", "C", "E",)))
+            self.assertEqual(membershipsB, set(("A",)))
+            self.assertEqual(membershipsC, set(("A",)))
+            self.assertEqual(membershipsD, set())
+            self.assertEqual(membershipsE, set(("A",)))
+
+    @inlineCallbacks
+    def test_cachingDBRemove(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "D",))
+            yield self.db.setGroupMembers("X", ("B", "C",))
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set(("B", "C", "D",)))
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set(("A", "X",)))
+            self.assertEqual(membershipsC, set(("A", "X",)))
+            self.assertEqual(membershipsD, set(("A",)))
+            
+            # Remove and check the result
+            yield self.db.removeGroup("A")
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set())
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set("X",))
+            self.assertEqual(membershipsC, set("X",))
+            self.assertEqual(membershipsD, set())
+
+    @inlineCallbacks
+    def test_cachingDBRemoveSpecial(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "D",))
+            yield self.db.setGroupMembers("X", ("B", "C",))
+    
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+            
+            # Remove and check the result
+            yield self.db.removeGroup("A")
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set())
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set("X",))
+            self.assertEqual(membershipsC, set("X",))
+            self.assertEqual(membershipsD, set())
+
+    @inlineCallbacks
+    def test_cachingDBRemovePrincipal(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result
+            yield self.db.setGroupMembers("A", ("B", "C", "D",))
+            yield self.db.setGroupMembers("X", ("B", "C",))
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set(("B", "C", "D",)))
+            self.assertEqual(membersX, set(("B", "C",)))
+            self.assertEqual(membershipsB, set(("A", "X",)))
+            self.assertEqual(membershipsC, set(("A", "X",)))
+            self.assertEqual(membershipsD, set(("A",)))
+            
+            # Remove and check the result
+            yield self.db.removePrincipal("B")
+    
+            membersA = yield self.db.getMembers("A")
+            membersX = yield self.db.getMembers("X")
+            membershipsB = yield self.db.getMemberships("B")
+            membershipsC = yield self.db.getMemberships("C")
+            membershipsD = yield self.db.getMemberships("D")
+    
+            self.assertEqual(membersA, set(("C", "D",)))
+            self.assertEqual(membersX, set(("C",)))
+            self.assertEqual(membershipsB, set())
+            self.assertEqual(membershipsC, set(("A", "X",)))
+            self.assertEqual(membershipsD, set(("A",),))
+
+    @inlineCallbacks
+    def test_cachingDBInsertUncached(self):
+    
+        for processType in ("Single", "Combined",):
+            config.ProcessType = processType
+
+            # Get the DB
+            yield self.db.clean()
+            
+            # Do one insert and check the result for the one we will remove
+            yield self.db.setGroupMembers("AA", ("BB", "CC", "DD",))
+            yield self.db.getMemberships("DD")
+    
+            # Change and check the result
+            yield self.db.setGroupMembers("AA", ("BB", "CC", "EE",))
+    
+            membersAA = yield self.db.getMembers("AA")
+            membershipsBB = yield self.db.getMemberships("BB")
+            membershipsCC = yield self.db.getMemberships("CC")
+            membershipsDD = yield self.db.getMemberships("DD")
+            membershipsEE = yield self.db.getMemberships("EE")
+    
+            self.assertEqual(membersAA, set(("BB", "CC", "EE",)))
+            self.assertEqual(membershipsBB, set(("AA",)))
+            self.assertEqual(membershipsCC, set(("AA",)))
+            self.assertEqual(membershipsDD, set())
+            self.assertEqual(membershipsEE, set(("AA",)))
+
+
+try:
+    import pgdb
+except ImportError:
+    ProxyPrincipalDBPostgreSQL.skip = True
+else:
+    try:
+        db = pgdb.connect(host="localhost", database="proxies")
+    except:
+        ProxyPrincipalDBPostgreSQL.skip = True

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -19,7 +19,8 @@
 from twisted.web2.dav import davxml
 
 from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile,\
+    proxiesFile
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 from twistedcaldav.directory.principal import DirectoryPrincipalResource
 from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
@@ -27,16 +28,22 @@
 
 import twistedcaldav.test.util
 from twistedcaldav.config import config
+from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
+import os
 
-
 class ProxyPrincipals (twistedcaldav.test.util.TestCase):
     """
     Directory service provisioned principals.
     """
+    
+    @inlineCallbacks
     def setUp(self):
         super(ProxyPrincipals, self).setUp()
 
         self.directoryService = XMLDirectoryService({'xmlFile' : xmlFile})
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+        calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB(self.mktemp())
 
         # Set up a principals hierarchy for each service we're testing with
         self.principalRootResources = {}
@@ -49,6 +56,10 @@
 
         self.principalRootResources[self.directoryService.__class__.__name__] = provisioningResource
 
+        config.DataRoot = self.mktemp()
+        os.mkdir(config.DataRoot)
+        yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
+
     def _getPrincipalByShortName(self, type, name):
         provisioningResource = self.principalRootResources[self.directoryService.__class__.__name__]
         return provisioningResource.principalForShortName(type, name)
@@ -278,7 +289,6 @@
             set(["5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1",
                  "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0"]))
 
-
     @inlineCallbacks
     def test_setGroupMemberSetNotifiesPrincipalCaches(self):
         class StubCacheNotifier(object):
@@ -295,7 +305,7 @@
         oldCacheNotifier = DirectoryPrincipalResource.cacheNotifierFactory
 
         try:
-            DirectoryPrincipalResource.cacheNotifierFactory = (lambda _1, _2: notifier)
+            DirectoryPrincipalResource.cacheNotifierFactory = (lambda _1, _2, **kwargs: notifier)
 
             self.assertEquals(notifier.changedCount, 0)
 
@@ -387,6 +397,7 @@
 
         # Set up the in-memory (non-null) memcacher:
         config.ProcessType = "Single"
+        calendaruserproxy.ProxyDBService._memcacher._memcacheProtocol = None
         principal = self._getPrincipalByShortName(
             DirectoryService.recordType_users, "wsanchez")
         db = principal._calendar_user_proxy_index()

Deleted: CalendarServer/trunk/twistedcaldav/directory/test/test_sqldb.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_sqldb.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_sqldb.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,51 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-##
-
-import os
-
-from twisted.python.filepath import FilePath
-
-#import twistedcaldav.directory.test.util
-#import twistedcaldav.directory.test.test_xmlfile
-#from twistedcaldav.directory.sqldb import SQLDirectoryService
-
-xmlFile = FilePath(os.path.join(os.path.dirname(__file__), "accounts.xml"))
-
-# FIXME: Add tests for GUID hooey, once we figure out what that means here
-
-# class SQLDB (
-#     twistedcaldav.directory.test.test_xmlfile.XMLFileBase,
-#     twistedcaldav.directory.test.util.BasicTestCase,
-#     twistedcaldav.directory.test.util.DigestTestCase
-# ):
-#     """
-#     Test SQL directory implementation.
-#     """
-#     def service(self):
-#         return SQLDirectoryService(os.getcwd(), self.xmlFile())
-# 
-#     def test_verifyCredentials_digest(self):
-#         super(SQLDB, self).test_verifyCredentials_digest()
-#     test_verifyCredentials_digest.todo = ""
-# 
-#     def test_verifyRealmFromDB(self):
-#         # Make sure the database has been initialized with the XML file
-#         self.service()
-# 
-#         # Then get an instance without using the XML file
-#         service = SQLDirectoryService(os.getcwd(), None)
-# 
-#         self.assertEquals(service.realmName, "Test")

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_xmlfile.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_xmlfile.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_xmlfile.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -17,16 +17,15 @@
 import os
 
 from twisted.python.filepath import FilePath
-from twisted.internet.defer import inlineCallbacks
 
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
+from twistedcaldav.directory import augment
 from twistedcaldav.directory.directory import DirectoryService
 import twistedcaldav.directory.test.util
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
-from twistedcaldav.config import config
 
 xmlFile = FilePath(os.path.join(os.path.dirname(__file__), "accounts.xml"))
+augmentsFile = FilePath(os.path.join(os.path.dirname(__file__), "augments.xml"))
+proxiesFile = FilePath(os.path.join(os.path.dirname(__file__), "proxies.xml"))
 
 # FIXME: Add tests for GUID hooey, once we figure out what that means here
 
@@ -88,6 +87,12 @@
             xmlFile.copyTo(self._xmlFile)
         return self._xmlFile
 
+    def augmentsFile(self):
+        if not hasattr(self, "_augmentsFile"):
+            self._augmentsFile = FilePath(self.mktemp())
+            augmentsFile.copyTo(self._augmentsFile)
+        return self._augmentsFile
+
 class XMLFile (
     XMLFileBase,
     twistedcaldav.directory.test.util.BasicTestCase,
@@ -97,7 +102,9 @@
     Test XML file based directory implementation.
     """
     def service(self):
-        return XMLDirectoryService({'xmlFile' : self.xmlFile()}, alwaysStat=True)
+        directory = XMLDirectoryService({'xmlFile' : self.xmlFile()}, alwaysStat=True)
+        augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,))
+        return directory
 
     def test_changedXML(self):
         service = self.service()
@@ -130,7 +137,6 @@
                 set(expectedRecords)
             )
 
-    @inlineCallbacks
     def test_okAutoSchedule(self):
         service = self.service()
 
@@ -143,11 +149,25 @@
     <guid>myoffice</guid>
     <password>nimda</password>
     <name>Super User</name>
-    <auto-schedule/>
   </location>
 </accounts>
 """
         )
+        self.augmentsFile().open("w").write(
+"""<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE accounts SYSTEM "accounts.dtd">
+<augments>
+  <record>
+    <guid>myoffice</guid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+</augments>
+"""
+        )
+        augment.AugmentService.refresh()
+
         for recordType, expectedRecords in (
             ( DirectoryService.recordType_users     , ()             ),
             ( DirectoryService.recordType_groups    , ()             ),
@@ -162,8 +182,7 @@
                 set(r.shortNames[0] for r in service.listRecords(recordType)),
                 set(expectedRecords)
             )
-        resourceInfoDatabase = ResourceInfoDatabase(config.DataRoot)
-        self.assertTrue((yield resourceInfoDatabase.getAutoSchedule(service.recordWithShortName(DirectoryService.recordType_locations, "my office").guid)))
+        self.assertTrue(service.recordWithShortName(DirectoryService.recordType_locations, "my office").autoSchedule)
 
 
     def test_okDisableCalendar(self):
@@ -182,11 +201,11 @@
     <uid>disabled</uid>
     <password>disabled</password>
     <name>Disabled</name>
-    <disable-calendar/>
   </group>
 </accounts>
 """
         )
+        
         for recordType, expectedRecords in (
             ( DirectoryService.recordType_users     , ()                       ),
             ( DirectoryService.recordType_groups    , ("enabled", "disabled")  ),
@@ -206,50 +225,3 @@
         self.assertFalse(service.recordWithShortName(DirectoryService.recordType_groups, "enabled").enabledForCalendaring)
         self.assertFalse(service.recordWithShortName(DirectoryService.recordType_groups, "disabled").enabledForCalendaring)
 
-    @inlineCallbacks
-    def test_okProxies(self):
-        service = self.service()
-
-        self.xmlFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<accounts realm="Test Realm">
-  <user>
-    <uid>test</uid>
-    <guid>test</guid>
-    <password>nimda</password>
-    <name>Test</name>
-  </user>
-  <location>
-    <uid>my office</uid>
-    <guid>myoffice</guid>
-    <password>nimda</password>
-    <name>Super User</name>
-    <auto-schedule/>
-    <proxies>
-        <member>test</member>
-    </proxies>
-  </location>
-</accounts>
-"""
-        )
-        for recordType, expectedRecords in (
-            ( DirectoryService.recordType_users     , ("test",)      ),
-            ( DirectoryService.recordType_groups    , ()             ),
-            ( DirectoryService.recordType_locations , ("my office",) ),
-            ( DirectoryService.recordType_resources , ()             ),
-        ):
-            # Fault records in
-            for name in expectedRecords:
-                service.recordWithShortName(recordType, name)
-
-            self.assertEquals(
-                set(r.shortNames[0] for r in service.listRecords(recordType)),
-                set(expectedRecords)
-            )
-        calendarUserProxyDatabase = CalendarUserProxyDatabase(config.DataRoot)
-        members = (yield calendarUserProxyDatabase.getMembers("myoffice#calendar-proxy-write"))
-        self.assertTrue("test" in members)
-        members = (yield calendarUserProxyDatabase.getMemberships("test"))
-        self.assertTrue("myoffice#calendar-proxy-write" in members)
-

Modified: CalendarServer/trunk/twistedcaldav/directory/xmlaccountsparser.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/xmlaccountsparser.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/xmlaccountsparser.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -27,13 +27,8 @@
 
 from twisted.python.filepath import FilePath
 
-from twistedcaldav.config import config
 from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.log import Logger
-from twistedcaldav.sql import DatabaseError
-from twistedcaldav.directory.directory import DirectoryError
-from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
 
 log = Logger()
 
@@ -52,15 +47,14 @@
 ELEMENT_EMAIL_ADDRESS     = "email-address"
 ELEMENT_MEMBERS           = "members"
 ELEMENT_MEMBER            = "member"
-ELEMENT_AUTOSCHEDULE      = "auto-schedule"
-ELEMENT_DISABLECALENDAR   = "disable-calendar"
-ELEMENT_PROXIES           = "proxies"
-ELEMENT_READ_ONLY_PROXIES = "read-only-proxies"
 
 ATTRIBUTE_REALM           = "realm"
 ATTRIBUTE_REPEAT          = "repeat"
 ATTRIBUTE_RECORDTYPE      = "type"
 
+VALUE_TRUE                = "true"
+VALUE_FALSE               = "false"
+
 RECORD_TYPES = {
     ELEMENT_USER     : DirectoryService.recordType_users,
     ELEMENT_GROUP    : DirectoryService.recordType_groups,
@@ -75,7 +69,7 @@
     def __repr__(self):
         return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
 
-    def __init__(self, xmlFile):
+    def __init__(self, xmlFile, externalUpdate=True):
 
         if type(xmlFile) is str:
             xmlFile = FilePath(xmlFile)
@@ -98,46 +92,7 @@
             log.error("Ignoring file %r because it is not a repository builder file" % (self.xmlFile,))
             return
         self._parseXML(accounts_node)
-        try:
-            self._updateExternalDatabases()
-        except DatabaseError, e:
-            raise DirectoryError(e)
 
-    def _updateExternalDatabases(self):
-        resourceInfoDatabase = ResourceInfoDatabase(config.DataRoot)
-
-        calendarUserProxyDatabase = CalendarUserProxyDatabase(config.DataRoot)
-
-        for records in self.items.itervalues():
-            for principal in records.itervalues():
-
-                resourceInfoDatabase.setAutoScheduleInDatabase(principal.guid,
-                    principal.autoSchedule)
-
-                if principal.proxies:
-                    proxies = []
-                    for recordType, uid in principal.proxies:
-                        record = self.items[recordType].get(uid)
-                        if record is not None:
-                            proxies.append(record.guid)
-
-                    calendarUserProxyDatabase.setGroupMembersInDatabase(
-                        "%s#calendar-proxy-write" % (principal.guid,),
-                        proxies
-                    )
-
-                if principal.readOnlyProxies:
-                    readOnlyProxies = []
-                    for recordType, uid in principal.readOnlyProxies:
-                        record = self.items[recordType].get(uid)
-                        if record is not None:
-                            readOnlyProxies.append(record.guid)
-
-                    calendarUserProxyDatabase.setGroupMembersInDatabase(
-                        "%s#calendar-proxy-read" % (principal.guid,),
-                        readOnlyProxies
-                    )
-
     def _parseXML(self, node):
         """
         Parse the XML root node from the accounts configuration document.
@@ -153,19 +108,6 @@
                 if item is not None:
                     item.groups.add(group.shortNames[0])
 
-        def updateProxyFor(proxier):
-            # Update proxy membership
-            for recordType, shortName in proxier.proxies:
-                item = self.items[recordType].get(shortName)
-                if item is not None:
-                    item.proxyFor.add((proxier.recordType, proxier.shortNames[0]))
-
-            # Update read-only proxy membership
-            for recordType, shortName in proxier.readOnlyProxies:
-                item = self.items[recordType].get(shortName)
-                if item is not None:
-                    item.readOnlyProxyFor.add((proxier.recordType, proxier.shortNames[0]))
-
         for child in node._get_childNodes():
             child_name = child._get_localName()
             if child_name is None:
@@ -194,7 +136,6 @@
         for records in self.items.itervalues():
             for principal in records.itervalues():
                 updateMembership(principal)
-                updateProxyFor(principal)
                 
 class XMLAccountRecord (object):
     """
@@ -214,16 +155,6 @@
         self.emailAddresses = set()
         self.members = set()
         self.groups = set()
-        self.calendarUserAddresses = set()
-        self.autoSchedule = False
-        if recordType == DirectoryService.recordType_groups:
-            self.enabledForCalendaring = False
-        else:
-            self.enabledForCalendaring = True
-        self.proxies = set()
-        self.proxyFor = set()
-        self.readOnlyProxies = set()
-        self.readOnlyProxyFor = set()
 
     def repeat(self, ctr):
         """
@@ -273,10 +204,6 @@
         result.lastName = lastName
         result.emailAddresses = emailAddresses
         result.members = self.members
-        result.autoSchedule = self.autoSchedule
-        result.enabledForCalendaring = self.enabledForCalendaring
-        result.proxies = self.proxies
-        result.readOnlyProxies = self.readOnlyProxies
         return result
 
     def parseXML(self, node):
@@ -309,24 +236,11 @@
                     self.emailAddresses.add(child.firstChild.data.encode("utf-8").lower())
             elif child_name == ELEMENT_MEMBERS:
                 self._parseMembers(child, self.members)
-            elif child_name == ELEMENT_AUTOSCHEDULE:
-                self.autoSchedule = True
-            elif child_name == ELEMENT_DISABLECALENDAR:
-                # FIXME: Not sure I see why this restriction is needed. --wsanchez
-                ## Only Users or Groups
-                #if self.recordType != DirectoryService.recordType_users:
-                #    raise ValueError("<disable-calendar> element only allowed for Users: %s" % (child_name,))
-                self.enabledForCalendaring = False
-            elif child_name == ELEMENT_PROXIES:
-                self._parseMembers(child, self.proxies)
-            elif child_name == ELEMENT_READ_ONLY_PROXIES:
-                self._parseMembers(child, self.readOnlyProxies)
             else:
                 raise RuntimeError("Unknown account attribute: %s" % (child_name,))
 
-        if self.enabledForCalendaring:
-            for email in self.emailAddresses:
-                self.calendarUserAddresses.add("mailto:%s" % (email,))
+        if not self.shortNames:
+            self.shortNames.append(self.guid)
 
     def _parseMembers(self, node, addto):
         for child in node._get_childNodes():

Copied: CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/directory/xmlaugmentsparser.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/xmlaugmentsparser.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,137 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from xml.etree.ElementTree import ElementTree
+from xml.parsers.expat import ExpatError
+import types
+
+"""
+XML based augment configuration file handling.
+"""
+
+__all__ = [
+    "XMLAugmentsParser",
+]
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+ELEMENT_AUGMENTS          = "augments"
+ELEMENT_RECORD            = "record"
+
+ELEMENT_GUID              = "guid"
+ELEMENT_ENABLE            = "enable"
+ELEMENT_HOSTEDAT          = "hosted-at"
+ELEMENT_ENABLECALENDAR    = "enable-calendar"
+ELEMENT_AUTOSCHEDULE      = "auto-schedule"
+
+ATTRIBUTE_REPEAT          = "repeat"
+
+VALUE_TRUE                = "true"
+VALUE_FALSE               = "false"
+
+ELEMENT_AUGMENTRECORD_MAP = {
+    ELEMENT_GUID:           "guid",
+    ELEMENT_ENABLE:         "enabled",
+    ELEMENT_HOSTEDAT:       "hostedAt",
+    ELEMENT_ENABLECALENDAR: "enabledForCalendaring",
+    ELEMENT_AUTOSCHEDULE:   "autoSchedule",
+}
+
+class XMLAugmentsParser(object):
+    """
+    XML augments configuration file parser.
+    """
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+    def __init__(self, xmlFile, items):
+
+        self.items = items
+        self.xmlFile = xmlFile
+
+        # Read in XML
+        try:
+            tree = ElementTree(file=self.xmlFile)
+        except ExpatError, e:
+            log.error("Unable to parse file '%s' because: %s" % (self.xmlFile, e,), raiseException=RuntimeError)
+
+        # Verify that top-level element is correct
+        augments_node = tree.getroot()
+        if augments_node.tag != ELEMENT_AUGMENTS:
+            log.error("Ignoring file '%s' because it is not a augments file" % (self.xmlFile,), raiseException=RuntimeError)
+
+        self._parseXML(augments_node)
+
+    def _parseXML(self, rootnode):
+        """
+        Parse the XML root node from the augments configuration document.
+        @param rootnode: the L{Element} to parse.
+        """
+        for child in rootnode.getchildren():
+            
+            if child.tag != ELEMENT_RECORD:
+                log.error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, self.xmlFile,), raiseException=RuntimeError)
+
+            repeat = int(child.get(ATTRIBUTE_REPEAT, "1"))
+
+            fields = {}
+            for node in child.getchildren():
+                
+                if node.tag in (
+                    ELEMENT_GUID,
+                    ELEMENT_HOSTEDAT,
+                ):
+                    fields[node.tag] = node.text
+                elif node.tag in (
+                    ELEMENT_ENABLE,
+                    ELEMENT_ENABLECALENDAR,
+                    ELEMENT_AUTOSCHEDULE,
+                ):
+                    fields[node.tag] = node.text == VALUE_TRUE
+                else:
+                    log.error("Invalid element '%s' in augment file: '%s'" % (node.tag, self.xmlFile,), raiseException=RuntimeError)
+                    
+            # Must have at least a guid
+            if ELEMENT_GUID not in fields:
+                log.error("Invalid record '%s' without a guid in augment file: '%s'" % (child, self.xmlFile,), raiseException=RuntimeError)
+                
+            if repeat > 1:
+                for i in xrange(1, repeat+1):
+                    self.buildRecord(fields, i)
+            else:
+                self.buildRecord(fields)
+    
+    def buildRecord(self, fields, count=None):
+        
+        from twistedcaldav.directory.augment import AugmentRecord
+
+        def expandCount(value, count):
+            
+            if type(value) in types.StringTypes:
+                return value % (count,) if count and "%" in value else value
+            elif type(value) == set:
+                return set([item % (count,) if count and "%" in item else item for item in value])
+            else:
+                return value
+        
+        actualFields = {}
+        for k,v in fields.iteritems():
+            actualFields[ELEMENT_AUGMENTRECORD_MAP[k]] = expandCount(v, count)
+
+        record = AugmentRecord(**actualFields)
+        self.items[record.guid] = record

Modified: CalendarServer/trunk/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/xmlfile.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/directory/xmlfile.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -29,6 +29,7 @@
 from twisted.web2.auth.digest import DigestedCredentials
 from twisted.python.filepath import FilePath
 
+from twistedcaldav.directory import augment
 from twistedcaldav.directory.directory import DirectoryService
 from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
     CachingDirectoryRecord
@@ -85,17 +86,29 @@
                     xmlPrincipal  = xmlPrincipal,
                 )
 
+                record = XMLDirectoryRecord(
+                    service       = self,
+                    recordType    = recordType,
+                    shortNames    = tuple(xmlPrincipal.shortNames),
+                    xmlPrincipal  = xmlPrincipal,
+                )
+                
+                # Look up augment information
+                # TODO: this needs to be deferred but for now we hard code the deferred result because
+                # we know it is completing immediately.
+                d = augment.AugmentService.getAugmentRecord(record.guid)
+                d.addCallback(lambda x:record.addAugmentInformation(x))
+
                 matched = False
                 if indexType == self.INDEX_TYPE_GUID:
-                    matched = indexKey == xmlPrincipal.guid
+                    matched = indexKey == record.guid
                 elif indexType == self.INDEX_TYPE_SHORTNAME:
-                    matched = indexKey in xmlPrincipal.shortNames
+                    matched = indexKey in record.shortNames
                 elif indexType == self.INDEX_TYPE_CUA:
                     matched = indexKey in record.calendarUserAddresses
                 
                 if matched:
-                    self.recordCacheForType(recordType).addRecord(
-                        record, indexType, indexKey)
+                    self.recordCacheForType(recordType).addRecord(record, indexType, indexKey)
             
     def recordsMatchingFields(self, fields, operand="or", recordType=None):
         # Default, brute force method search of underlying XML data
@@ -191,7 +204,6 @@
             firstName             = xmlPrincipal.firstName,
             lastName              = xmlPrincipal.lastName,
             emailAddresses        = xmlPrincipal.emailAddresses,
-            enabledForCalendaring = xmlPrincipal.enabledForCalendaring,
         )
 
         self.password          = xmlPrincipal.password
@@ -207,9 +219,10 @@
             yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
 
     def verifyCredentials(self, credentials):
-        if isinstance(credentials, UsernamePassword):
-            return credentials.password == self.password
-        if isinstance(credentials, DigestedCredentials):
-            return credentials.checkPassword(self.password)
+        if self.enabled:
+            if isinstance(credentials, UsernamePassword):
+                return credentials.password == self.password
+            if isinstance(credentials, DigestedCredentials):
+                return credentials.checkPassword(self.password)
 
         return super(XMLDirectoryRecord, self).verifyCredentials(credentials)

Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/extensions.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.test.test_extensions -*-
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -442,17 +442,18 @@
 
             for record in records:
                 resource = principalCollection.principalForRecord(record)
-                matchingResources.append(resource)
+                if resource:
+                    matchingResources.append(resource)
+    
+                    # We've determined this is a matching resource
+                    matchcount += 1
+                    if clientLimit is not None and matchcount >= clientLimit:
+                        resultsWereLimited = ("client", matchcount)
+                        break
+                    if matchcount >= max_number_of_matches:
+                        resultsWereLimited = ("server", matchcount)
+                        break
 
-                # We've determined this is a matching resource
-                matchcount += 1
-                if clientLimit is not None and matchcount >= clientLimit:
-                    resultsWereLimited = ("client", matchcount)
-                    break
-                if matchcount >= max_number_of_matches:
-                    resultsWereLimited = ("server", matchcount)
-                    break
-
         # Generate the response
         responses = []
         for resource in matchingResources:

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -1242,8 +1242,8 @@
         timezone_refs    = set()
         timezones        = set()
         got_master       = False
-       #got_override     = False
-       #master_recurring = False
+        #got_override     = False
+        #master_recurring = False
         
         for subcomponent in self.subcomponents():
             # Disallowed in CalDAV-Access-08, section 4.1
@@ -1291,15 +1291,14 @@
                         raise ValueError(msg)
                     else:
                         got_master = True
-                        # master_recurring =
-                        subcomponent.hasProperty("RRULE") or subcomponent.hasProperty("RDATE")
+                        # master_recurring = subcomponent.hasProperty("RRULE") or subcomponent.hasProperty("RDATE")
                 else:
                     pass # got_override = True
                             
                 # Check that if an override is present then the master is recurring
                 # Leopard iCal sometimes does this for overridden instances that an Attendee receives and
                 # it creates a "fake" (invalid) master. We are going to skip this test here. Instead implicit
-                # scheduling with verify the validity of the components and raise if they don't make sense.
+                # scheduling will verify the validity of the components and raise if they don't make sense.
                 # If no scheduling is happening then we allow this - that may cause other clients to choke.
                 # If it does we will have to reinstate this check but only after we have checked for implicit.
 # UNCOMMENT OUT master_recurring AND got_override ASSIGNMENTS ABOVE
@@ -2089,7 +2088,7 @@
                     if dataValue.find(dropboxPrefix) != -1:
                         self.removeProperty(attachment)
 
-    def normalizeCalendarUserAddresses(self, lookupFunction):
+    def normalizeCalendarUserAddresses(self, lookupFunction, toUUID=True):
         """
         Do the ORGANIZER/ATTENDEE property normalization.
 
@@ -2111,9 +2110,48 @@
                 if guid is None:
                     continue
 
-                # Always re-write value to urn:uuid
-                prop.setValue("urn:uuid:%s" % (guid,))
+                # Get any EMAIL parameter
+                oldemail = prop.params().get("EMAIL", (None,))[0]
+                if oldemail:
+                    oldemail = "mailto:%s" % (oldemail,)
 
+                if toUUID:
+                    # Always re-write value to urn:uuid
+                    prop.setValue("urn:uuid:%s" % (guid,))
+                    
+                # If it is already a non-UUID address leave it be
+                elif cuaddr.startswith("urn:uuid:"):
+                    if oldemail:
+                        # Use the EMAIL parameter if it exists
+                        newaddr = oldemail
+                    else:
+                        # Pick the first mailto, or failing that the first http, or failing that the first one
+                        first_mailto = None
+                        first_http = None
+                        first = None
+                        for addr in cuaddrs:
+                            if addr.startswith("mailto:"):
+                                first_mailto = addr
+                                break
+                            elif addr.startswith("http:"):
+                                if not first_http:
+                                    first_http = addr
+                            elif not first:
+                                first = addr
+                        
+                        if first_mailto:
+                            newaddr = first_mailto
+                        elif first_http:
+                            newaddr = first_http
+                        elif first:
+                            newaddr = first
+                        else:
+                            newaddr = None
+                    
+                    # Make the change
+                    if newaddr:
+                        prop.setValue(newaddr)
+
                 # Always re-write the CN parameter
                 if name:
                     prop.params()["CN"] = [name,]
@@ -2123,12 +2161,8 @@
                     except KeyError:
                         pass
 
-                # Re-write the EMAIL if its value no longer
-                # matches
-                oldemail = prop.params().get("EMAIL", (None,))[0]
-                if oldemail:
-                    oldemail = "mailto:%s" % (oldemail,)
-                if oldemail is None or oldemail not in cuaddrs:
+                # Re-write the EMAIL if its value no longer matches
+                if oldemail and oldemail not in cuaddrs or oldemail is None and toUUID:
                     if cuaddr.startswith("mailto:") and cuaddr in cuaddrs:
                         email = cuaddr[7:]
                     else:

Modified: CalendarServer/trunk/twistedcaldav/index.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/index.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/index.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -828,7 +828,7 @@
         assert resource.isCalendarCollection(), "non-calendar collection resource %s has no index." % (resource,)
         super(Index, self).__init__(resource)
 
-        if config.Memcached['ClientEnabled']:
+        if config.Memcached.Pools.Default.ClientEnabled:
             self.reserver = MemcachedUIDReserver(self)
 
         else:

Modified: CalendarServer/trunk/twistedcaldav/log.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/log.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/log.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -352,8 +352,10 @@
     #
     # Attach methods to Logger
     #
-    def log_emit(self, message, __level__=level, **kwargs):
+    def log_emit(self, message, __level__=level, raiseException=None, **kwargs):
         self.emit(__level__, message, **kwargs)
+        if raiseException:
+            raise raiseException(message)
 
     def will_emit(self, __level__=level):
         return self.willLogAtLevel(__level__)
@@ -369,8 +371,10 @@
     #
     # Attach methods to LoggingMixIn
     #
-    def log_emit(self, message, __level__=level, **kwargs):
+    def log_emit(self, message, __level__=level, raiseException=None, **kwargs):
         self.logger.emit(__level__, message, **kwargs)
+        if raiseException:
+            raise raiseException(message)
 
     def will_emit(self=log_emit, __level__=level):
         return self.logger.willLogAtLevel(__level__)

Modified: CalendarServer/trunk/twistedcaldav/mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/mail.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/mail.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -544,15 +544,10 @@
 
     def makeService(self, options):
 
-        if config.Memcached.ClientEnabled:
-            memcachepool.installPool(
-                IPv4Address(
-                    "TCP",
-                    config.Memcached.BindAddress,
-                    config.Memcached.Port,
-                ),
-                config.Memcached.MaxClients,
-            )
+        memcachepool.installPools(
+            config.Memcached.Pools,
+            config.Memcached.MaxClients,
+        )
 
         multiService = service.MultiService()
 

Modified: CalendarServer/trunk/twistedcaldav/memcachepool.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/memcachepool.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/memcachepool.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2010 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.
@@ -15,6 +15,7 @@
 ##
 
 from twisted.python.failure import Failure
+from twisted.internet.address import IPv4Address
 from twisted.internet.defer import Deferred, fail
 from twisted.internet.protocol import ReconnectingClientFactory
 
@@ -370,22 +371,46 @@
     @ivar _cachePool: A saved cachePool.
     """
     _cachePool = None
+    _cachePoolHandle = "Default"
 
     def getCachePool(self):
         if self._cachePool is None:
-            return defaultCachePool()
+            return defaultCachePool(self._cachePoolHandle)
 
         return self._cachePool
 
 
 
-_memCachePool = None
+_memCachePools = {}         # Maps a name to a pool object
+_memCachePoolHandler = {}   # Maps a handler id to a named pool
 
-def installPool(serverAddress, maxClients=5, reactor=None):
-    global _memCachePool
-    _memCachePool = MemCachePool(serverAddress,
+def installPools(pools, maxClients=5, reactor=None):
+    
+    for name, pool in pools.items():
+        if pool["ClientEnabled"]:
+            _installPool(
+                name,
+                pool["HandleCacheTypes"],
+                IPv4Address(
+                    "TCP",
+                    pool["BindAddress"],
+                    pool["Port"],
+                ),
+                maxClients,
+                reactor,
+            )
+
+def _installPool(name, handleTypes, serverAddress, maxClients=5, reactor=None):
+
+    pool = MemCachePool(serverAddress,
                                  maxClients=maxClients,
                                  reactor=None)
+    _memCachePools[name] = pool
 
-def defaultCachePool():
-    return _memCachePool
+    for handle in handleTypes:
+        _memCachePoolHandler[handle] = pool
+
+def defaultCachePool(name):
+    if name not in _memCachePoolHandler:
+        name = "Default"
+    return _memCachePoolHandler[name]

Modified: CalendarServer/trunk/twistedcaldav/memcacheprops.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/memcacheprops.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/memcacheprops.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009 Apple Computer, Inc. All rights reserved.
+# Copyright (c) 2009-2010 Apple Computer, Inc. All rights reserved.
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -60,7 +60,9 @@
 
             log.info("Instantiating memcache connection for MemcachePropertyCollection")
 
-            MemcachePropertyCollection._memcacheClient = MemcacheClientFactory.getClient(["%s:%s" % (config.Memcached.BindAddress, config.Memcached.Port)],
+            MemcachePropertyCollection._memcacheClient = MemcacheClientFactory.getClient([
+                    "%s:%s" % (config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port)
+                ],
                 debug=0,
                 pickleProtocol=2,
             )

Modified: CalendarServer/trunk/twistedcaldav/memcacher.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/memcacher.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/memcacher.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2010 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.
@@ -107,6 +107,7 @@
         
         assert len(namespace) <= Memcacher.NAMESPACE_MAX_LENGTH, "Memcacher namespace must be less than or equal to %s characters long" % (Memcacher.NAMESPACE_MAX_LENGTH,)
         self._memcacheProtocol = None
+        self._cachePoolHandle = namespace
         self._namespace = namespace
         self._pickle = pickle
         self._noInvalidation = no_invalidation
@@ -116,7 +117,7 @@
         if self._memcacheProtocol is not None:
             return self._memcacheProtocol
 
-        if config.Memcached['ClientEnabled']:
+        if config.Memcached.Pools.Default.ClientEnabled:
             self._memcacheProtocol = self.getCachePool()
 
         elif config.ProcessType == "Single" or self._noInvalidation or self.allowTestCache:

Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -290,7 +290,7 @@
 
 @inlineCallbacks
 def generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchtotal,
-                         excludeuid=None, organizer=None, same_calendar_user=False,
+                         excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False,
                          servertoserver=False):
     """
     Run a free busy report on the specified calendar collection
@@ -313,7 +313,7 @@
     # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
     if not servertoserver:
         try:
-            yield calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),))
+            yield calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), principal=organizerPrincipal)
         except AccessDeniedError:
             returnValue(matchtotal)
 
@@ -371,7 +371,7 @@
         # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
         if not servertoserver:
             try:
-                yield child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces)
+                yield child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces, principal=organizerPrincipal)
             except AccessDeniedError:
                 continue
 

Modified: CalendarServer/trunk/twistedcaldav/notify.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/notify.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/notify.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2008 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -1301,13 +1301,10 @@
         #
         # Configure Memcached Client Pool
         #
-        if config.Memcached.ClientEnabled:
-            memcachepool.installPool(
-                IPv4Address(
-                    'TCP',
-                    config.Memcached.BindAddress,
-                    config.Memcached.Port),
-                config.Memcached.MaxClients)
+        memcachepool.installPools(
+            config.Memcached.Pools,
+            config.Memcached.MaxClients,
+        )
 
         multiService = service.MultiService()
 

Copied: CalendarServer/trunk/twistedcaldav/partitions.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/partitions.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/partitions.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/partitions.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,71 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from twext.python.plistlib import readPlist
+from twistedcaldav.client.pool import installPool
+from twistedcaldav.log import Logger
+
+"""
+Collection of classes for managing partition information for a group of servers.
+"""
+
+log = Logger()
+
+class Partitions(object):
+
+    def __init__(self):
+        
+        self.clear()
+
+    def clear(self):
+        self.partitions = {}
+        self.ownUID = ""
+        self.maxClients = 5
+
+    def readConfig(self, plistpath):
+        try:
+            dataDict = readPlist(plistpath)
+        except (IOError, OSError):                                    
+            log.error("Configuration file does not exist or is inaccessible: %s" % (self._configFileName,))
+            return
+        
+        for partition in dataDict.get("partitions", ()):
+            uid = partition.get("uid", None)
+            url = partition.get("url", None)
+            if uid and url:
+                self.partitions[uid] = url
+
+    def setSelfPartition(self, uid):
+        self.ownUID = uid
+
+    def setMaxClients(self, maxClients):
+        self.maxClients = maxClients
+
+    def getPartitionURL(self, uid):
+        # When the UID matches this server return an empty string
+        return self.partitions.get(uid, None) if uid != self.ownUID else ""
+
+    def installReverseProxies(self):
+        
+        for partition, url in self.partitions.iteritems():
+            if partition != self.ownUID:
+                installPool(
+                    partition,
+                    url,
+                    self.maxClients,
+                )
+
+partitions = Partitions()

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -905,7 +905,7 @@
                 ))
 
             elif name == "auto-schedule":
-                autoSchedule = (yield self.getAutoSchedule())
+                autoSchedule = self.getAutoSchedule()
                 returnValue(customxml.AutoSchedule("true" if autoSchedule else "false"))
 
         result = (yield super(CalendarPrincipalResource, self).readProperty(property, request))
@@ -916,8 +916,8 @@
             "%r is not a WebDAVElement instance" % (property,)
         )
 
-        if property.qname() == (caldav_namespace, "auto-schedule"):
-            self.setAutoSchedule(autoSchedule)
+        #if property.qname() == (caldav_namespace, "auto-schedule"):
+        #    self.setAutoSchedule(autoSchedule)
 
         return super(CalendarPrincipalResource, self).writeProperty(property, request)
 

Modified: CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -22,7 +22,8 @@
 from twistedcaldav.scheduling.imip import ScheduleViaIMip
 from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
 from twistedcaldav.scheduling.cuaddress import LocalCalendarUser,\
-    RemoteCalendarUser, EmailCalendarUser, InvalidCalendarUser
+    RemoteCalendarUser, EmailCalendarUser, InvalidCalendarUser,\
+    PartitionedCalendarUser
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 __all__ = [
@@ -49,17 +50,14 @@
     @inlineCallbacks
     def getCalendarUser(self, cuaddr, principal):
         
-        # If we have a principal always treat the user as local
+        # If we have a principal always treat the user as local or partitioned
         if principal:
-            returnValue(LocalCalendarUser(cuaddr, principal))
+            returnValue(LocalCalendarUser(cuaddr, principal) if principal.locallyHosted() else PartitionedCalendarUser(cuaddr, principal))
 
         # Get the type
         cuaddr_type = (yield self.getCalendarUserServiceType(cuaddr))
         if cuaddr_type == DeliveryService.serviceType_caldav:
-            if principal:
-                returnValue(LocalCalendarUser(cuaddr, principal))
-            else:
-                returnValue(InvalidCalendarUser(cuaddr))
+            returnValue(InvalidCalendarUser(cuaddr))
         elif cuaddr_type == DeliveryService.serviceType_ischedule:
             returnValue(RemoteCalendarUser(cuaddr))
         elif cuaddr_type == DeliveryService.serviceType_imip:

Modified: CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -38,7 +38,8 @@
 from twistedcaldav.log import Logger
 from twistedcaldav.method import report_common
 from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser,\
+    PartitionedCalendarUser
 from twistedcaldav.scheduling.delivery import DeliveryService
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
 from twistedcaldav.scheduling.processing import ImplicitProcessor, ImplicitProcessorException
@@ -73,7 +74,7 @@
         if cuaddr.startswith("mailto:") and config.Scheduling[cls.serviceType()]["EmailDomain"]:
             addr = cuaddr[7:].split("?")[0]
             domain = config.Scheduling[cls.serviceType()]["EmailDomain"]
-            account, addrDomain = addr.split("@")
+            _ignore_account, addrDomain = addr.split("@")
             if addrDomain == domain:
                 return True
 
@@ -98,7 +99,7 @@
         uid = self.scheduler.calendar.resourceUID()
 
         organizerPrincipal = None
-        if type(self.scheduler.organizer) in (LocalCalendarUser,):
+        if type(self.scheduler.organizer) in (LocalCalendarUser, PartitionedCalendarUser,):
             organizerPrincipal = davxml.Principal(davxml.HRef(self.scheduler.organizer.principal.principalURL()))
 
         for recipient in self.recipients:
@@ -125,7 +126,7 @@
 
             # Different behavior for free-busy vs regular invite
             if self.freebusy:
-                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, uid)
+                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid)
             else:
                 yield self.generateResponse(recipient, self.responses)
 
@@ -196,7 +197,7 @@
                 returnValue(True)
     
     @inlineCallbacks
-    def generateFreeBusyResponse(self, recipient, responses, organizerProp, uid):
+    def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid):
 
         # Extract the ATTENDEE property matching current recipient from the calendar data
         cuas = recipient.principal.calendarUserAddresses()
@@ -208,6 +209,7 @@
             fbresult = (yield self.generateAttendeeFreeBusyResponse(
                 recipient,
                 organizerProp,
+                organizerPrincipal,
                 uid,
                 attendeeProp,
                 remote,
@@ -222,7 +224,7 @@
             returnValue(True)
     
     @inlineCallbacks
-    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, uid, attendeeProp, remote):
+    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote):
 
         # Find the current recipients calendar-free-busy-set
         fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
@@ -261,6 +263,7 @@
                 matchtotal,
                 excludeuid = self.scheduler.excludeUID,
                 organizer = self.scheduler.organizer.cuaddr,
+                organizerPrincipal = organizerPrincipal,
                 same_calendar_user = same_calendar_user,
                 servertoserver=remote,
             ))

Modified: CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -19,6 +19,7 @@
 
 __all__ = [
     "LocalCalendarUser",
+    "PartitionedCalendarUser",
     "RemoteCalendarUser",
     "EmailCalendarUser",
     "InvalidCalendarUser",
@@ -43,6 +44,15 @@
     def __str__(self):
         return "Local calendar user: %s" % (self.cuaddr,)
 
+class PartitionedCalendarUser(CalendarUser):
+    def __init__(self, cuaddr, principal):
+        self.cuaddr = cuaddr
+        self.principal = principal
+        self.serviceType = DeliveryService.serviceType_ischedule
+
+    def __str__(self):
+        return "Partitioned calendar user: %s" % (self.cuaddr,)
+
 class RemoteCalendarUser(CalendarUser):
     def __init__(self, cuaddr):
         self.cuaddr = cuaddr

Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -26,17 +26,17 @@
 from twistedcaldav import caldavxml
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.customxml import TwistedSchedulingObjectResource
+from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
 from twistedcaldav.ical import Property
 from twistedcaldav.log import Logger
 from twistedcaldav.method import report_common
 from twistedcaldav.scheduling import addressmapping
 from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
-    LocalCalendarUser
+    LocalCalendarUser, PartitionedCalendarUser
 from twistedcaldav.scheduling.icaldiff import iCalDiff
 from twistedcaldav.scheduling.itip import iTipGenerator, iTIPRequestStatus
 from twistedcaldav.scheduling.scheduler import CalDAVScheduler
 from twistedcaldav.scheduling.utils import getCalendarObjectForPrincipals
-from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
 
 __all__ = [
     "ImplicitScheduler",
@@ -316,7 +316,7 @@
         # Get some useful information from the calendar
         yield self.extractCalendarData()        
 
-        self.attendee = attendee.cuaddr
+        self.originator = self.attendee = attendee.cuaddr
         self.attendeePrincipal = attendee.principal
         
         result = (yield self.scheduleWithOrganizer())
@@ -327,6 +327,8 @@
     def extractCalendarData(self):
         
         # Get the originator who is the authenticated user
+        # TODO: the originator actually needs to be the owner of the calendar collection not the authenticated
+        # principal, who might be a proxy or admin
         self.originatorPrincipal = None
         self.originator = ""
         authz_principal = self.resource.currentPrincipal(self.request).children[0]
@@ -877,6 +879,10 @@
         calendar_resource, _ignore_name, _ignore_collection, _ignore_uri = (yield getCalendarObjectForPrincipals(self.request, self.organizerPrincipal, self.uid))
         if calendar_resource:
             self.organizer_calendar = calendar_resource.iCalendar()
+        elif isinstance(self.organizerAddress, PartitionedCalendarUser):
+            # For partitioning where the organizer is on a different node, we will assume that the attendee's copy
+            # of the event is up to date and "authoritative". So we pretend that is the organizer copy
+            self.organizer_calendar = self.oldcalendar
         
     def isAttendeeChangeInsignificant(self):
         """

Modified: CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -24,7 +24,7 @@
 from twisted.web2 import responsecode
 from twisted.web2.client.http import ClientRequest
 from twisted.web2.client.http import HTTPClientProtocol
-from twisted.web2.dav.util import davXMLFromStream
+from twisted.web2.dav.util import davXMLFromStream, joinURL
 from twisted.web2.http import HTTPError
 from twisted.web2.http_headers import Headers
 from twisted.web2.http_headers import MimeType
@@ -36,9 +36,12 @@
 from twistedcaldav.config import config
 from twistedcaldav.log import Logger
 from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
+from twistedcaldav.scheduling.ischeduleservers import IScheduleServers,\
+    IScheduleServerRecord
 from twistedcaldav.scheduling.itip import iTIPRequestStatus
 from twistedcaldav.util import utf8String
+from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser,\
+    PartitionedCalendarUser
 
 import OpenSSL
 
@@ -66,7 +69,7 @@
         # Do default match
         return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
 
-    def generateSchedulingResponses(self):
+    def generateSchedulingResponses(self, refreshOnly=False):
         """
         Generate scheduling responses for remote recipients.
         """
@@ -76,8 +79,13 @@
         groups = {}
         servermgr = IScheduleServers()
         for recipient in self.recipients:
-            # Map the recipient's domain to a server
-            server = servermgr.mapDomain(recipient.domain)
+            if isinstance(recipient, RemoteCalendarUser):
+                # Map the recipient's domain to a server
+                server = servermgr.mapDomain(recipient.domain)
+            elif isinstance(recipient, PartitionedCalendarUser):
+                server = self._getServerForPartitionedUser(recipient)
+            else:
+                assert False, "Incorrect calendar user address class"
             if not server:
                 # Cannot do server-to-server for this recipient.
                 err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
@@ -104,19 +112,32 @@
         # rather than serialize them.
         deferreds = []
         for server, recipients in groups.iteritems():
-            requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses)
+            requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses, refreshOnly)
             deferreds.append(requestor.doRequest())
 
         return DeferredList(deferreds)
 
+    def _getServerForPartitionedUser(self, recipient):
+        
+        if not hasattr(self, "partitionedServers"):
+            self.partitionedServers = {}
+            
+        partition = recipient.principal.hostedURL()
+        if partition not in self.partitionedServers:
+            self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
+            self.partitionedServers[partition].unNormalizeAddresses = False
+        
+        return self.partitionedServers[partition]
+
 class IScheduleRequest(object):
     
-    def __init__(self, scheduler, server, recipients, responses):
+    def __init__(self, scheduler, server, recipients, responses, refreshOnly=False):
 
         self.scheduler = scheduler
         self.server = server
         self.recipients = recipients
         self.responses = responses
+        self.refreshOnly = refreshOnly
         
         self._generateHeaders()
         self._prepareData()
@@ -152,12 +173,17 @@
     def _generateHeaders(self):
         self.headers = Headers()
         self.headers.setHeader('Host', utf8String(self.server.host + ":%s" % (self.server.port,)))
-        self.headers.addRawHeader('Originator', utf8String(self.scheduler.originator.cuaddr))
+        
+        # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
+        self.headers.addRawHeader('Originator', utf8String(self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee))
         self._doAuthentication()
         for recipient in self.recipients:
             self.headers.addRawHeader('Recipient', utf8String(recipient.cuaddr))
         self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
 
+        if self.refreshOnly:
+            self.headers.addRawHeader("X-CALENDARSERVER-ITIP-REFRESHONLY", "T")
+
     def _doAuthentication(self):
         if self.server.authentication and self.server.authentication[0] == "basic":
             self.headers.setHeader(
@@ -166,8 +192,22 @@
             )
 
     def _prepareData(self):
-        self.data = str(self.scheduler.calendar)
+        if self.server.unNormalizeAddresses and self.scheduler.method == "PUT": 
+            def lookupFunction(cuaddr):
+                principal = self.scheduler.resource.principalForCalendarUserAddress(cuaddr)
+                if principal is None:
+                    return (None, None, None)
+                else:
+                    return (principal.record.fullName.decode("utf-8"),
+                        principal.record.guid,
+                        principal.record.calendarUserAddresses)
 
+            normalizedCalendar = self.scheduler.calendar.duplicate()
+            normalizedCalendar.normalizeCalendarUserAddresses(lookupFunction, toUUID=False)
+        else:
+            normalizedCalendar = self.scheduler.calendar
+        self.data = str(normalizedCalendar)
+
     def _parseResponse(self, xml):
 
         # Check for correct root element

Modified: CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 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.
@@ -123,7 +123,7 @@
     """
     Contains server-to-server details.
     """
-    def __init__(self):
+    def __init__(self, uri=None):
         """
         @param recordType: record type for directory entry.
         """
@@ -133,6 +133,11 @@
         self.allow_to = True
         self.domains = []
         self.client_hosts = []
+        self.unNormalizeAddresses = True
+        
+        if uri:
+            self.uri = uri
+            self._parseDetails()
 
     def parseXML(self, node):
         for child in node._get_childNodes():

Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -283,7 +283,7 @@
                 raise ImplicitProcessorException(iTIPRequestStatus.NO_USER_SUPPORT)
 
             log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - new processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
-            autoprocessed = (yield self.recipient.principal.getAutoSchedule())
+            autoprocessed = self.recipient.principal.getAutoSchedule()
             new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr, autoprocessing=autoprocessed)
             name =  md5(str(new_calendar) + str(time.time()) + default.fp.path).hexdigest() + ".ics"
             
@@ -306,7 +306,7 @@
             result = (True, autoprocessed, changes,)
         else:
             # Processing update to existing event
-            autoprocessed = (yield self.recipient.principal.getAutoSchedule())
+            autoprocessed = self.recipient.principal.getAutoSchedule()
             new_calendar, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr, autoprocessing=autoprocessed)
             if new_calendar:
      
@@ -367,7 +367,7 @@
         else:
             # Need to check for auto-respond attendees. These need to suppress the inbox message
             # if the cancel is processed.
-            autoprocessed = (yield self.recipient.principal.getAutoSchedule())
+            autoprocessed = self.recipient.principal.getAutoSchedule()
 
             # Check to see if this is a cancel of the entire event
             processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar, autoprocessing=autoprocessed)
@@ -506,7 +506,7 @@
                         tr.start = makeTimedUTC(instance.start)
                         tr.end = makeTimedUTC(instance.end)
 
-                        yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid)
+                        yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid, servertoserver=True)
                         
                         # If any fbinfo entries exist we have an overlap
                         if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):

Modified: CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2008 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -36,7 +36,8 @@
 from twistedcaldav.scheduling import addressmapping
 from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
 from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
-    LocalCalendarUser, RemoteCalendarUser, EmailCalendarUser
+    LocalCalendarUser, RemoteCalendarUser, EmailCalendarUser,\
+    PartitionedCalendarUser
 from twistedcaldav.scheduling.imip import ScheduleViaIMip
 from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
 from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
@@ -45,6 +46,7 @@
 import itertools
 import re
 import socket
+import urlparse
 
 """
 CalDAV/Server-to-Server scheduling behavior.
@@ -70,6 +72,8 @@
         self.recipients = None
         self.calendar = None
         self.organizer = None
+        self.attendee = None
+        self.isiTIPRequest = None
         self.timeRange = None
         self.excludeUID = None
         self.fakeTheResult = False
@@ -247,7 +251,7 @@
     def checkOriginator(self):
         raise NotImplementedError
 
-    def checkRecipient(self):
+    def checkRecipients(self):
         raise NotImplementedError
 
     def checkOrganizer(self):
@@ -282,6 +286,27 @@
             log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component %s request: %s" % (self.method, self.calendar,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
     
+        # Determine iTIP method mode
+        if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+            self.isiTIPRequest = True
+
+        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+            self.isiTIPRequest = False
+
+            # Verify that there is a single ATTENDEE property
+            attendees = self.calendar.getAttendees()
+        
+            # Must have only one
+            if len(attendees) != 1:
+                log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendardata,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+            self.attendee = attendees[0]
+
+        else:
+            msg = "Unknown iTIP METHOD: %s" % (self.calendar.propertyValue("METHOD"),)
+            log.err(msg)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description=msg))
+
     def checkForFreeBusy(self):
         if not hasattr(self, "isfreebusy"):
             if (self.calendar.propertyValue("METHOD") == "REQUEST") and (self.calendar.mainType() == "VFREEBUSY"):
@@ -362,6 +387,7 @@
     
         # Loop over each recipient and aggregate into lists by service types.
         caldav_recipients = []
+        partitioned_recipients = []
         remote_recipients = []
         imip_recipients = []
         for recipient in self.recipients:
@@ -372,6 +398,9 @@
             elif isinstance(recipient, LocalCalendarUser):
                 caldav_recipients.append(recipient)
 
+            elif isinstance(recipient, PartitionedCalendarUser):
+                partitioned_recipients.append(recipient)
+
             elif isinstance(recipient, RemoteCalendarUser):
                 remote_recipients.append(recipient)
 
@@ -386,6 +415,10 @@
         if caldav_recipients:
             yield self.generateLocalSchedulingResponses(caldav_recipients, responses, freebusy)
 
+        # Now process partitioned recipients
+        if partitioned_recipients:
+            yield self.generateRemoteSchedulingResponses(partitioned_recipients, responses, freebusy, getattr(self.request, 'doing_attendee_refresh', False))
+
         # To reduce chatter, we suppress certain messages
         if not getattr(self.request, 'suppressRefresh', False):
 
@@ -409,14 +442,14 @@
         requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
         return requestor.generateSchedulingResponses()
 
-    def generateRemoteSchedulingResponses(self, recipients, responses, freebusy):
+    def generateRemoteSchedulingResponses(self, recipients, responses, freebusy, refreshOnly=False):
         """
         Generate scheduling responses for remote recipients.
         """
 
         # Create the scheduler and run it.
         requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
-        return requestor.generateSchedulingResponses()
+        return requestor.generateSchedulingResponses(refreshOnly)
 
     def generateIMIPSchedulingResponses(self, recipients, responses, freebusy):
         """
@@ -499,10 +532,10 @@
                 inbox = None
                 inboxURL = principal.scheduleInboxURL()
                 if inboxURL:
-                    inbox = (yield self.request.locateResource(inboxURL))
+                    inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
 
                 if inbox:
-                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL))
+                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
                 else:
                     log.err("No schedule inbox for principal: %s" % (principal,))
                     results.append(InvalidCalendarUser(recipient))
@@ -559,17 +592,8 @@
         Only local attendees are allowed for message originating from this server.
         """
         
-        # Verify that there is a single ATTENDEE property
-        attendees = self.calendar.getAttendees()
-    
-        # Must have only one
-        if len(attendees) != 1:
-            log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-        attendee = attendees[0]
-    
         # Attendee's Outbox MUST be the request URI
-        attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
+        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
         if attendeePrincipal:
             if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != self.request.uri:
                 log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendar,))
@@ -584,16 +608,12 @@
         """
     
         # Prevent spoofing of ORGANIZER with specific METHODs when local
-        if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+        if self.isiTIPRequest:
             self.checkOrganizerAsOriginator()
     
         # Prevent spoofing when doing reply-like METHODs
-        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+        else:
             self.checkAttendeeAsOriginator()
-            
-        else:
-            log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Unknown iTIP METHOD for security checks"))
 
     def finalChecks(self):
         """
@@ -618,8 +638,62 @@
             else:
                 raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid iTIP message for implicit scheduling"))
 
-class IScheduleScheduler(Scheduler):
+class RemoteScheduler(Scheduler):
 
+    def checkOrganizer(self):
+        """
+        Delay ORGANIZER check until we know what their role is.
+        """
+        pass
+
+    @inlineCallbacks
+    def checkRecipients(self):
+        """
+        Check the validity of the Recipient header values. These must all be local as there
+        is no concept of server-to-server relaying.
+        """
+        
+        results = []
+        for recipient in self.recipients:
+            # Get the principal resource for this recipient
+            principal = self.resource.principalForCalendarUserAddress(recipient)
+            
+            # If no principal we may have a remote recipient but we should check whether
+            # the address is one that ought to be on our server and treat that as a missing
+            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+            if principal is None:
+                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(recipient))
+                if localUser:
+                    log.err("No principal for calendar user address: %s" % (recipient,))
+                else:
+                    log.err("Unknown calendar user address: %s" % (recipient,))
+                results.append(InvalidCalendarUser(recipient))
+            else:
+                # Map recipient to their inbox
+                inbox = None
+                inboxURL = principal.scheduleInboxURL()
+                if inboxURL:
+                    inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
+
+                if inbox:
+                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
+                else:
+                    log.err("No schedule inbox for principal: %s" % (principal,))
+                    results.append(InvalidCalendarUser(recipient))
+        
+        self.recipients = results
+
+class IScheduleScheduler(RemoteScheduler):
+
+    def loadFromRequestHeaders(self):
+        """
+        Load Originator and Recipient from request headers.
+        """
+        super(IScheduleScheduler, self).loadFromRequestHeaders()
+        
+        if self.request.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
+            self.request.doing_attendee_refresh = 1
+        
     def checkAuthorization(self):
         # Must have an unauthenticated user
         if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
@@ -636,11 +710,21 @@
         originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
         localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
         if originatorPrincipal or localUser:
-            log.err("Cannot use originator that is on this server: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+            if originatorPrincipal.locallyHosted():
+                log.err("Cannot use originator that is on this server: %s" % (self.originator,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+            else:
+                self.originator = PartitionedCalendarUser(self.originator, originatorPrincipal)
+                #self._validPartitionServer()
         else:
             self.originator = RemoteCalendarUser(self.originator)
-            
+            self._validiScheduleServer()
+
+    def _validiScheduleServer(self):
+        """
+        Check the validity of the iSchedule host.
+        """
+    
         # We will only accept originator in known domains.
         servermgr = IScheduleServers()
         server = servermgr.mapDomain(self.originator.domain)
@@ -684,49 +768,38 @@
                 log.err("Originator not on allowed server: %s" % (self.originator,))
                 raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
 
-    @inlineCallbacks
-    def checkRecipients(self):
+    def _validPartitionServer(self, principal):
         """
-        Check the validity of the Recipient header values. These must all be local as there
-        is no concept of server-to-server relaying.
+        Check the validity of the partitioned host.
         """
-        
-        results = []
-        for recipient in self.recipients:
-            # Get the principal resource for this recipient
-            principal = self.resource.principalForCalendarUserAddress(recipient)
-            
-            # If no principal we may have a remote recipient but we should check whether
-            # the address is one that ought to be on our server and treat that as a missing
-            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
-            if principal is None:
-                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(recipient))
-                if localUser:
-                    log.err("No principal for calendar user address: %s" % (recipient,))
-                else:
-                    log.err("Unknown calendar user address: %s" % (recipient,))
-                results.append(InvalidCalendarUser(recipient))
-            else:
-                # Map recipient to their inbox
-                inbox = None
-                inboxURL = principal.scheduleInboxURL()
-                if inboxURL:
-                    inbox = (yield self.request.locateResource(inboxURL))
 
-                if inbox:
-                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL))
-                else:
-                    log.err("No schedule inbox for principal: %s" % (principal,))
-                    results.append(InvalidCalendarUser(recipient))
+        # Extract expected host/port
+        expected_uri = principal.hostedURL()
+        expected_uri = urlparse.urlparse(expected_uri)
+    
+        # Get the request IP and map to hostname.
+        clientip = self.request.remoteAddr.host
         
-        self.recipients = results
+        # First compare as dotted IP
+        matched = False
+        if clientip == expected_uri.hostname:
+            matched = True
+        else:
+            # Now do hostname lookup
+            try:
+                host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+                for hostname in itertools.chain((host,), aliases):
+                    # Try host match
+                    if hostname == expected_uri.hostname:
+                        matched = True
+                        break
+            except socket.herror, e:
+                log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
+        
+        if not matched:
+            log.err("Originator not on allowed server: %s" % (self.originator,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
 
-    def checkOrganizer(self):
-        """
-        Delay ORGANIZER check until we know what their role is.
-        """
-        pass
-
     @inlineCallbacks
     def checkOrganizerAsOriginator(self):
         """
@@ -738,8 +811,13 @@
         if organizer:
             organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
             if organizerPrincipal:
-                log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+                if organizerPrincipal.locallyHosted():
+                    log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+                else:
+                    # Check that the origin server is the correct partition
+                    self.organizer = PartitionedCalendarUser(organizer, organizerPrincipal)
+                    self._validPartitionServer(self.organizer.principal)
             else:
                 localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
                 if localUser:
@@ -758,22 +836,16 @@
         Only local attendees are allowed for message originating from this server.
         """
         
-        # Verify that there is a single ATTENDEE property
-        attendees = self.calendar.getAttendees()
-    
-        # Must have only one
-        if len(attendees) != 1:
-            log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-        attendee = attendees[0]
-    
         # Attendee cannot be local.
-        attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
+        attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
         if attendeePrincipal:
-            log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+            if attendeePrincipal.locallyHosted():
+                log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+            else:
+                self._validPartitionServer(attendeePrincipal)                
         else:
-            localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(attendee))
+            localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
             if localUser:
                 log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
                 raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
@@ -826,13 +898,25 @@
         pass
 
 
-class IMIPScheduler(Scheduler):
+class IMIPScheduler(RemoteScheduler):
 
     def checkAuthorization(self):
         pass
 
-    def checkOrganizer(self):
-        pass
+    @inlineCallbacks
+    def checkOriginator(self):
+        """
+        Check the validity of the Originator header.
+        """
+    
+        # For remote requests we do not allow the originator to be a local user or one within our domain.
+        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
+        if originatorPrincipal or localUser:
+            log.err("Cannot use originator that is on this server: %s" % (self.originator,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        else:
+            self.originator = RemoteCalendarUser(self.originator)
 
     def checkOrganizerAsOriginator(self):
         pass
@@ -856,60 +940,7 @@
             # TODO: verify this is the right response:
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
 
-    @inlineCallbacks
-    def checkOriginator(self):
-        """
-        Check the validity of the Originator header.
-        """
-    
-        # For remote requests we do not allow the originator to be a local user or one within our domain.
-        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
-        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
-        if originatorPrincipal or localUser:
-            log.err("Cannot use originator that is on this server: %s" % (self.originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-        else:
-            self.originator = RemoteCalendarUser(self.originator)
 
-    @inlineCallbacks
-    def checkRecipients(self):
-        """
-        Check the validity of the Recipient header values. These must all be local as there
-        is no concept of server-to-server relaying.
-        """
-        
-        results = []
-        for recipient in self.recipients:
-            # Get the principal resource for this recipient
-            principal = self.resource.principalForCalendarUserAddress(recipient)
-            
-            # If no principal we may have a remote recipient but we should check whether
-            # the address is one that ought to be on our server and treat that as a missing
-            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
-            if principal is None:
-                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(recipient))
-                if localUser:
-                    log.err("No principal for calendar user address: %s" % (recipient,))
-                else:
-                    log.err("Unknown calendar user address: %s" % (recipient,))
-                results.append(InvalidCalendarUser(recipient))
-            else:
-                # Map recipient to their inbox
-                inbox = None
-                inboxURL = principal.scheduleInboxURL()
-                if inboxURL:
-                    inbox = (yield self.request.locateResource(inboxURL))
-
-                if inbox:
-                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL))
-                else:
-                    log.err("No schedule inbox for principal: %s" % (principal,))
-                    results.append(InvalidCalendarUser(recipient))
-        
-        self.recipients = results
-
-
-
 class ScheduleResponseResponse (Response):
     """
     ScheduleResponse L{Response} object.

Modified: CalendarServer/trunk/twistedcaldav/scheduling/utils.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/utils.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/scheduling/utils.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2005-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -23,12 +23,14 @@
     Get a copy of the event for a principal.
     """
     
-    result = {}
-    result["resource"] = None
-    result["resource_name"] = None
-    result["calendar_collection"] = None
-    result["calendar_collection_uri"] = None
-    if principal:
+    result = {
+        "resource": None,
+        "resource_name": None,
+        "calendar_collection": None,
+        "calendar_collection_uri": None,
+    }
+
+    if principal and principal.locallyHosted():
         # Get principal's calendar-home
         calendar_home = principal.calendarHome()
         

Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/static.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -54,11 +54,12 @@
 from twisted.web2.dav.noneprops import NonePropertyStore
 from twisted.web2.dav.resource import AccessDeniedError
 from twisted.web2.dav.resource import davPrivilegeSet
-from twisted.web2.dav.util import parentForURL, bindMethods
+from twisted.web2.dav.util import parentForURL, bindMethods, joinURL
 
 from twistedcaldav import caldavxml
 from twistedcaldav import customxml
 from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.client.reverseproxy import ReverseProxyResource
 from twistedcaldav.config import config
 from twistedcaldav.customxml import TwistedCalendarAccessProperty, TwistedScheduleMatchETags
 from twistedcaldav.extensions import DAVFile, CachingPropertyStore
@@ -736,60 +737,77 @@
 
         assert len(name) > 4, "Directory record has an invalid GUID: %r" % (name,)
         
-        childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
-        child = self.homeResourceClass(childPath.path, self, record)
-
-        if not child.exists():
-            self.provision()
-
-            if not childPath.parent().isdir():
-                childPath.parent().makedirs()
-
-            for oldPath in (
-                # Pre 2.0: All in one directory
-                self.fp.child(name),
-                # Pre 1.2: In types hierarchy instead of the GUID hierarchy
-                self.parent.getChild(record.recordType).fp.child(record.shortNames[0]),
-            ):
-                if oldPath.exists():
-                    # The child exists at an old location.  Move to new location.
-                    log.msg("Moving calendar home from old location %r to new location %r." % (oldPath, childPath))
-                    try:
-                        oldPath.moveTo(childPath)
-                    except (OSError, IOError), e:
-                        log.err("Error moving calendar home %r: %s" % (oldPath, e))
+        if record.locallyHosted():
+            childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
+            child = self.homeResourceClass(childPath.path, self, record)
+    
+            if not child.exists():
+                self.provision()
+    
+                if not childPath.parent().isdir():
+                    childPath.parent().makedirs()
+    
+                for oldPath in (
+                    # Pre 2.0: All in one directory
+                    self.fp.child(name),
+                    # Pre 1.2: In types hierarchy instead of the GUID hierarchy
+                    self.parent.getChild(record.recordType).fp.child(record.shortNames[0]),
+                ):
+                    if oldPath.exists():
+                        # The child exists at an old location.  Move to new location.
+                        log.msg("Moving calendar home from old location %r to new location %r." % (oldPath, childPath))
+                        try:
+                            oldPath.moveTo(childPath)
+                        except (OSError, IOError), e:
+                            log.err("Error moving calendar home %r: %s" % (oldPath, e))
+                            raise HTTPError(StatusResponse(
+                                responsecode.INTERNAL_SERVER_ERROR,
+                                "Unable to move calendar home."
+                            ))
+                        child.fp.changed()
+                        break
+                else:
+                    #
+                    # NOTE: provisionDefaultCalendars() returns a deferred, which we are ignoring.
+                    # The result being that the default calendars will be present at some point
+                    # in the future, not necessarily right now, and we don't have a way to wait
+                    # on that to finish.
+                    #
+                    child.provisionDefaultCalendars()
+    
+                    #
+                    # Try to work around the above a little by telling the client that something
+                    # when wrong temporarily if the child isn't provisioned right away.
+                    #
+                    if not child.exists():
                         raise HTTPError(StatusResponse(
-                            responsecode.INTERNAL_SERVER_ERROR,
-                            "Unable to move calendar home."
+                            responsecode.SERVICE_UNAVAILABLE,
+                            "Provisioning calendar home."
                         ))
-                    child.fp.changed()
-                    break
-            else:
-                #
-                # NOTE: provisionDefaultCalendars() returns a deferred, which we are ignoring.
-                # The result being that the default calendars will be present at some point
-                # in the future, not necessarily right now, and we don't have a way to wait
-                # on that to finish.
-                #
-                child.provisionDefaultCalendars()
+    
+                assert child.exists()
+        
+        else:
+            childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
+            child = CalendarHomeReverseProxyFile(childPath.path, self, record)
 
-                #
-                # Try to work around the above a little by telling the client that something
-                # when wrong temporarily if the child isn't provisioned right away.
-                #
-                if not child.exists():
-                    raise HTTPError(StatusResponse(
-                        responsecode.SERVICE_UNAVAILABLE,
-                        "Provisioning calendar home."
-                    ))
-
-            assert child.exists()
-
         return child
 
     def createSimilarFile(self, path):
         raise HTTPError(responsecode.NOT_FOUND)
 
+class CalendarHomeReverseProxyFile(ReverseProxyResource):
+    
+    def __init__(self, path, parent, record):
+        self.path = path
+        self.parent = parent
+        self.record = record
+        
+        super(CalendarHomeReverseProxyFile, self).__init__(self.record.hostedAt)
+    
+    def url(self):
+        return joinURL(self.parent.url(), self.record.uid)
+
 class CalendarHomeFile (PropfindCacheMixin, AutoProvisioningFileMixIn, DirectoryCalendarHomeResource, CalDAVFile):
     """
     Calendar home collection resource.

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -28,6 +28,7 @@
     ConfigProvider, ConfigurationError, config, _mergeData, )
 from twistedcaldav.log import (
     Logger, clearLogLevels, setLogLevelForNamespace, InvalidLogLevelError, )
+from twistedcaldav.partitions import partitions
 from twistedcaldav.util import (
     KeychainAccessError, KeychainPasswordNotFound, getPasswordFromKeychain, )
 
@@ -41,12 +42,37 @@
     },
     "twistedcaldav.directory.appleopendirectory.OpenDirectoryService": {
         "node": "/Search",
-        "restrictEnabledRecords": False,
-        "restrictToGroup": "",
         "cacheTimeout": 30,
     },
 }
 
+DEFAULT_AUGMENT_PARAMS = {
+    "twistedcaldav.directory.augment.AugmentXMLDB": {
+        "xmlFiles": ["/etc/caldavd/augments.xml",],
+    },
+    "twistedcaldav.directory.augment.AugmentSqliteDB": {
+        "dbpath": "/etc/caldavd/augments.sqlite",
+    },
+    "twistedcaldav.directory.augment.AugmentPostgreSQLDB": {
+        "host":     "localhost",
+        "database": "augments",
+        "user":     "",
+        "password": "",
+    },
+}
+
+DEFAULT_PROXYDB_PARAMS = {
+    "twistedcaldav.directory.calendaruserproxy.ProxySqliteDB": {
+        "dbpath": "/etc/caldavd/proxies.sqlite",
+    },
+    "twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB": {
+        "host":     "localhost",
+        "database": "proxies",
+        "user":     "",
+        "password": "",
+    },
+}
+
 DEFAULT_CONFIG = {
     # Note: Don't use None values below; that confuses the command-line parser.
 
@@ -101,6 +127,25 @@
     },
 
     #
+    # Augment service
+    #
+    #    Augments for the directory service records to add calendar specific attributes.
+    #
+    "AugmentService": {
+        "type": "twistedcaldav.directory.augment.AugmentXMLDB",
+        "params": DEFAULT_AUGMENT_PARAMS["twistedcaldav.directory.augment.AugmentXMLDB"],
+    },
+
+    #
+    # Proxies
+    #
+    "ProxyDBService": {
+        "type": "twistedcaldav.directory.calendaruserproxy.ProxySqliteDB",
+        "params": DEFAULT_PROXYDB_PARAMS["twistedcaldav.directory.calendaruserproxy.ProxySqliteDB"],
+    },
+    "ProxyLoadFromFile": "",    # Allows for initialization of the proxy database from an XML file
+
+    #
     # Special principals
     #
     "AdminPrincipals": [],                       # Principals with "DAV:all" access (relative URLs)
@@ -316,6 +361,16 @@
     },
 
     #
+    # Partitioning
+    #
+    "Partitioning" : {
+        "Enabled":             False,   # Partitioning enabled or not
+        "ServerPartitionID":   "",      # Unique ID for this server's partition instance.
+        "PartitionConfigFile": "/etc/caldavd/partitions.plist", # File path for partition information
+        "MaxClients":          5,       # Pool size for connections to each partition
+    },
+
+    #
     # Performance tuning
     #
 
@@ -378,10 +433,26 @@
 
     "Memcached": {
         "MaxClients": 5,
-        "ClientEnabled": True,
-        "ServerEnabled": True,
-        "BindAddress": "127.0.0.1",
-        "Port": 11211,
+        "Pools": {
+            "Default": {
+                "ClientEnabled": True,
+                "ServerEnabled": True,
+                "BindAddress": "127.0.0.1",
+                "Port": 11211,
+                "HandleCacheTypes": [
+                    "Default",
+                ]
+            },
+#            "ProxyDB": {
+#                "ClientEnabled": True,
+#                "ServerEnabled": True,
+#                "BindAddress": "127.0.0.1",
+#                "Port": 11211,
+#                "HandleCacheTypes": [
+#                    "ProxyDB", "PrincipalToken",
+#                ]
+#            },
+        },
         "memcached": "memcached", # Find in PATH
         "MaxMemory": 0, # Megabytes
         "Options": [],
@@ -460,6 +531,20 @@
             if param not in DEFAULT_SERVICE_PARAMS[configDict.DirectoryService.type]:
                 del configDict.DirectoryService.params[param]
 
+def _postUpdateAugmentService(configDict):
+    if configDict.AugmentService.type in DEFAULT_AUGMENT_PARAMS:
+        for param in tuple(configDict.AugmentService.params):
+            if param not in DEFAULT_AUGMENT_PARAMS[configDict.AugmentService.type]:
+                log.warn("Parameter %s is not supported by service %s" % (param, configDict.AugmentService.type))
+                del configDict.AugmentService.params[param]
+
+def _postUpdateProxyDBService(configDict):
+    if configDict.ProxyDBService.type in DEFAULT_PROXYDB_PARAMS:
+        for param in tuple(configDict.ProxyDBService.params):
+            if param not in DEFAULT_PROXYDB_PARAMS[configDict.ProxyDBService.type]:
+                log.warn("Parameter %s is not supported by service %s" % (param, configDict.ProxyDBService.type))
+                del configDict.ProxyDBService.params[param]
+
 def _updateACLs(configDict):
     #
     # Base resource ACLs
@@ -555,9 +640,8 @@
     try:
         if "DefaultLogLevel" in configDict:
             level = configDict["DefaultLogLevel"]
-            if not level:
-                level = "warn"
-            setLogLevelForNamespace(None, level)
+            if level:
+                setLogLevelForNamespace(None, level)
 
         if "LogLevels" in configDict:
             for namespace in configDict["LogLevels"]:
@@ -650,19 +734,35 @@
                     # The password doesn't exist in the keychain.
                     log.info("iMIP %s password not found in keychain" %
                         (direction,))
-    
+
+def _updatePartitions(configDict):
+    #
+    # Partitions
+    #
+
+    if configDict.Partitioning.Enabled:
+        partitions.setSelfPartition(configDict.Partitioning.ServerPartitionID)
+        partitions.setMaxClients(configDict.Partitioning.MaxClients)
+        partitions.readConfig(configDict.Partitioning.PartitionConfigFile)
+        partitions.installReverseProxies()
+    else:
+        partitions.clear()
+
 PRE_UPDATE_HOOKS = (
     _preUpdateDirectoryService,
     )
 POST_UPDATE_HOOKS = (
     _updateHostName,
     _postUpdateDirectoryService,
+    _postUpdateAugmentService,
+    _postUpdateProxyDBService,
     _updateACLs,
     _updateRejectClients,
     _updateDropBox,
     _updateLogLevels,
     _updateNotifications,
     _updateScheduling,
+    _updatePartitions,
     )
     
 def _cleanup(configDict, defaultDict):

Modified: CalendarServer/trunk/twistedcaldav/test/test_cache.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_cache.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/test/test_cache.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2010 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.
@@ -336,7 +336,7 @@
         self.tokens['/principals/__uids__/cdaboo/'] = 'principalToken0'
         self.tokens['/principals/__uids__/dreid/'] = 'principalTokenX'
 
-        def _getToken(uri):
+        def _getToken(uri, cachePoolHandle=None):
             return succeed(self.tokens.get(uri))
 
         self.rc._tokenForURI = _getToken

Modified: CalendarServer/trunk/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_config.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/test/test_config.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -159,22 +159,15 @@
         self.assertEquals(config.DirectoryService.type, "twistedcaldav.directory.appleopendirectory.OpenDirectoryService")
         self.assertNotIn("xmlFile", config.DirectoryService.params)
         self.assertEquals(config.DirectoryService.params.node, "/Search")
-        self.assertEquals(config.DirectoryService.params.restrictEnabledRecords, False)
 
     def testDirectoryService_newParam(self):
         self.assertEquals(config.DirectoryService.type, "twistedcaldav.directory.xmlfile.XMLDirectoryService")
         self.assertEquals(config.DirectoryService.params.xmlFile, "/etc/caldavd/accounts.xml")
 
         config.update({"DirectoryService": {"type": "twistedcaldav.directory.appleopendirectory.OpenDirectoryService"}})
-        config.update({"DirectoryService": {"params": {
-            "restrictEnabledRecords": True,
-            "restrictToGroup": "12345",
-        }}})
 
         self.assertEquals(config.DirectoryService.type, "twistedcaldav.directory.appleopendirectory.OpenDirectoryService")
         self.assertEquals(config.DirectoryService.params.node, "/Search")
-        self.assertEquals(config.DirectoryService.params.restrictEnabledRecords, True)
-        self.assertEquals(config.DirectoryService.params.restrictToGroup, "12345")
 
     def testDirectoryService_unknownType(self):
         self.assertEquals(config.DirectoryService.type, "twistedcaldav.directory.xmlfile.XMLDirectoryService")
@@ -236,6 +229,9 @@
         """
         Logging module configures properly.
         """
+        config.setDefaults(DEFAULT_CONFIG)
+        config.reload()
+
         self.assertEquals(logLevelForNamespace(None), "warn")
         self.assertEquals(logLevelForNamespace("some.namespace"), "warn")
 

Copied: CalendarServer/trunk/twistedcaldav/test/test_database.py (from rev 4957, CalendarServer/branches/users/cdaboo/partition-4464/twistedcaldav/test/test_database.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_database.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_database.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -0,0 +1,208 @@
+##
+# Copyright (c) 2009-2010 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.
+##
+
+from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin
+import twistedcaldav.test.util
+
+from twisted.internet.defer import inlineCallbacks
+
+import os
+import time
+
+class Database (twistedcaldav.test.util.TestCase):
+    """
+    Test abstract SQL DB class
+    """
+    
+    class TestDB(ADBAPISqliteMixin, AbstractADBAPIDatabase):
+        
+        def __init__(self, path, persistent=False, version="1"):
+            self.version = version
+            self.dbpath = path
+            super(Database.TestDB, self).__init__("sqlite", "sqlite3", (path,), persistent, cp_min=3, cp_max=3)
+
+        def _db_version(self):
+            """
+            @return: the schema version assigned to this index.
+            """
+            return self.version
+            
+        def _db_type(self):
+            """
+            @return: the collection type assigned to this index.
+            """
+            return "TESTTYPE"
+            
+        def _db_init_data_tables(self):
+            """
+            Initialise the underlying database tables.
+            @param q:           a database cursor to use.
+            """
+    
+            #
+            # TESTTYPE table
+            #
+            return self._db_execute(
+                """
+                create table TESTTYPE (
+                    KEY         text unique,
+                    VALUE       text
+                )
+                """
+            )
+
+        def _db_remove_data_tables(self):
+            return self._db_execute("drop table TESTTYPE")
+
+    class TestDBRecreateUpgrade(TestDB):
+        
+        class RecreateDBException(Exception):
+            pass
+        class UpgradeDBException(Exception):
+            pass
+
+        def __init__(self, path, persistent=False):
+            super(Database.TestDBRecreateUpgrade, self).__init__(path, persistent, version="2")
+
+        def _db_recreate(self):
+            raise self.RecreateDBException()
+
+    class TestDBCreateIndexOnUpgrade(TestDB):
+        
+        def __init__(self, path, persistent=False):
+            super(Database.TestDBCreateIndexOnUpgrade, self).__init__(path, persistent, version="2")
+
+        def _db_upgrade_data_tables(self, old_version):
+            return self._db_execute(
+                """
+                create index TESTING on TESTTYPE (VALUE)
+                """
+            )
+
+    class TestDBPauseInInit(TestDB):
+        
+        def _db_init(self):
+            
+            time.sleep(1)
+            super(Database.TestDBPauseInInit, self)._db_init()
+
+    @inlineCallbacks
+    def inlineCallbackRaises(self, exc, f, *args, **kwargs):
+        try:
+            yield f(*args, **kwargs)
+        except exc:
+            pass
+        except Exception, e:
+            self.fail("Wrong exception raised: %s" % (e,))
+        else:
+            self.fail("%s not raised" % (exc,))
+
+    @inlineCallbacks
+    def test_connect(self):
+        """
+        Connect to database and create table
+        """
+        db = Database.TestDB(self.mktemp())
+        self.assertFalse(db.initialized)
+        yield db.open()
+        self.assertTrue(db.initialized)
+
+    @inlineCallbacks
+    def test_readwrite(self):
+        """
+        Add a record, search for it
+        """
+        db = Database.TestDB(self.mktemp())
+        yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", ("FOO", "BAR",))
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR"),))
+        items = (yield db.queryList("SELECT * from TESTTYPE"))
+        self.assertEqual(items, ("FOO",))
+
+    @inlineCallbacks
+    def test_close(self):
+        """
+        Close database
+        """
+        db = Database.TestDB(self.mktemp())
+        self.assertFalse(db.initialized)
+        yield db.open()
+        db.close()
+        self.assertFalse(db.initialized)
+        db.close()
+        
+    @inlineCallbacks
+    def test_version_upgrade_nonpersistent(self):
+        """
+        Connect to database and create table
+        """
+        
+        db_file = self.mktemp()
+
+        db = Database.TestDB(db_file)
+        yield db.open()
+        yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", ("FOO", "BAR",))
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR"),))
+        db.close()
+        db = None
+
+        db = Database.TestDBRecreateUpgrade(db_file)
+        yield self.inlineCallbackRaises(Database.TestDBRecreateUpgrade.RecreateDBException, db.open)
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, ())
+
+    def test_version_upgrade_persistent(self):
+        """
+        Connect to database and create table
+        """
+        db_file = self.mktemp()
+        db = Database.TestDB(db_file, persistent=True)
+        yield db.open()
+        yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", "FOO", "BAR")
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR")))
+        db.close()
+        db = None
+
+        db = Database.TestDBRecreateUpgrade(db_file, persistent=True)
+        yield self.inlineCallbackRaises(NotImplementedError, db.open)
+        self.assertTrue(os.path.exists(db_file))
+        db.close()
+        db = None
+
+        db = Database.TestDB(db_file, persistent=True, autocommit=True)
+        yield db.open()
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR")))
+
+    def test_version_upgrade_persistent_add_index(self):
+        """
+        Connect to database and create table
+        """
+        db_file = self.mktemp()
+        db = Database.TestDB(db_file, persistent=True, autocommit=True)
+        yield db.open()
+        yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", "FOO", "BAR")
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR")))
+        db.close()
+        db = None
+
+        db = Database.TestDBCreateIndexOnUpgrade(db_file, persistent=True, autocommit=True)
+        yield db.open()
+        items = (yield db.query("SELECT * from TESTTYPE"))
+        self.assertEqual(items, (("FOO", "BAR")))

Modified: CalendarServer/trunk/twistedcaldav/test/test_upgrade.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_upgrade.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/test/test_upgrade.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2008 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2010 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.
@@ -41,7 +41,11 @@
             "directory", "test", "accounts.xml")
         config.DirectoryService.params.xmlFile = xmlFile
 
+        xmlAugmentsFile = os.path.join(os.path.dirname(os.path.dirname(__file__)),
+            "directory", "test", "augments.xml")
+        config.AugmentService.params.xmlFiles = (xmlAugmentsFile,)
 
+
     def setUpInitialStates(self):
         self.setUpXMLDirectory()
 
@@ -429,10 +433,10 @@
             {
                 "@contents" : None,
             },
-            ResourceInfoDatabase.dbFilename :
-            {
-                "@contents" : None,
-            }
+#            ResourceInfoDatabase.dbFilename :
+#            {
+#                "@contents" : None,
+#            }
         }
 
         self.verifyDirectoryComparison(before, after, reverify=True)
@@ -794,10 +798,10 @@
             {
                 "@contents" : None,
             },
-            ResourceInfoDatabase.dbFilename :
-            {
-                "@contents" : None,
-            }
+#            ResourceInfoDatabase.dbFilename :
+#            {
+#                "@contents" : None,
+#            }
         }
 
         self.verifyDirectoryComparison(before, after, reverify=True)
@@ -914,10 +918,10 @@
             {
                 "@contents" : None,
             },
-            ResourceInfoDatabase.dbFilename :
-            {
-                "@contents" : None,
-            }
+#            ResourceInfoDatabase.dbFilename :
+#            {
+#                "@contents" : None,
+#            }
         }
 
         self.verifyDirectoryComparison(before, after, reverify=True)
@@ -1033,10 +1037,10 @@
             {
                 "@contents" : None,
             },
-            ResourceInfoDatabase.dbFilename :
-            {
-                "@contents" : None,
-            }
+#            ResourceInfoDatabase.dbFilename :
+#            {
+#                "@contents" : None,
+#            }
         }
 
         self.verifyDirectoryComparison(before, after)
@@ -1125,10 +1129,10 @@
             {
                 "@contents" : None,
             },
-            ResourceInfoDatabase.dbFilename :
-            {
-                "@contents" : None,
-            }
+#            ResourceInfoDatabase.dbFilename :
+#            {
+#                "@contents" : None,
+#            }
         }
 
 
@@ -1166,18 +1170,18 @@
             {
                 "@contents" : "1",
             },
-            CalendarUserProxyDatabase.dbFilename :
-            {
-                "@contents" : None,
-            },
+#            CalendarUserProxyDatabase.dbFilename :
+#            {
+#                "@contents" : None,
+#            },
             MailGatewayTokensDatabase.dbFilename :
             {
                 "@contents" : None,
             },
-            ResourceInfoDatabase.dbFilename :
-            {
-                "@contents" : None,
-            }
+#            ResourceInfoDatabase.dbFilename :
+#            {
+#                "@contents" : None,
+#            }
         }
         root = self.createHierarchy(before)
         config.DocumentRoot = root
@@ -1189,28 +1193,28 @@
         calendarUserProxyDatabase = CalendarUserProxyDatabase(root)
         resourceInfoDatabase = ResourceInfoDatabase(root)
 
-        for guid, info in assignments.iteritems():
+#        for guid, info in assignments.iteritems():
+#
+#            proxyGroup = "%s#calendar-proxy-write" % (guid,)
+#            result = set([row[0] for row in calendarUserProxyDatabase._db_execute("select MEMBER from GROUPS where GROUPNAME = :1", proxyGroup)])
+#            if info[1]:
+#                self.assertTrue(info[1] in result)
+#            else:
+#                self.assertTrue(not result)
+#
+#            readOnlyProxyGroup = "%s#calendar-proxy-read" % (guid,)
+#            result = set([row[0] for row in calendarUserProxyDatabase._db_execute("select MEMBER from GROUPS where GROUPNAME = :1", readOnlyProxyGroup)])
+#            if info[2]:
+#                self.assertTrue(info[2] in result)
+#            else:
+#                self.assertTrue(not result)
+#
+#            autoSchedule = resourceInfoDatabase._db_value_for_sql("select AUTOSCHEDULE from RESOURCEINFO where GUID = :1", guid)
+#            autoSchedule = autoSchedule == 1
+#            self.assertEquals(info[0], autoSchedule)
 
-            proxyGroup = "%s#calendar-proxy-write" % (guid,)
-            result = set([row[0] for row in calendarUserProxyDatabase._db_execute("select MEMBER from GROUPS where GROUPNAME = :1", proxyGroup)])
-            if info[1]:
-                self.assertTrue(info[1] in result)
-            else:
-                self.assertTrue(not result)
 
-            readOnlyProxyGroup = "%s#calendar-proxy-read" % (guid,)
-            result = set([row[0] for row in calendarUserProxyDatabase._db_execute("select MEMBER from GROUPS where GROUPNAME = :1", readOnlyProxyGroup)])
-            if info[2]:
-                self.assertTrue(info[2] in result)
-            else:
-                self.assertTrue(not result)
 
-            autoSchedule = resourceInfoDatabase._db_value_for_sql("select AUTOSCHEDULE from RESOURCEINFO where GUID = :1", guid)
-            autoSchedule = autoSchedule == 1
-            self.assertEquals(info[0], autoSchedule)
-
-
-
 event01_before = """BEGIN:VCALENDAR
 VERSION:2.0
 PRODID:-//Apple Inc.//iCal 3.0//EN

Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/test/util.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2010 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.
@@ -42,8 +42,8 @@
         dataroot = self.mktemp()
         os.mkdir(dataroot)
         config.DataRoot = dataroot
-        config.Memcached.ClientEnabled = False
-        config.Memcached.ServerEnabled = False
+        config.Memcached.Pools.Default.ClientEnabled = False
+        config.Memcached.Pools.Default.ServerEnabled = False
         memcacheclient.ClientFactory.allowTestCache = True
         memcacher.Memcacher.allowTestCache = True
 

Modified: CalendarServer/trunk/twistedcaldav/upgrade.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/upgrade.py	2010-01-21 20:46:14 UTC (rev 4957)
+++ CalendarServer/trunk/twistedcaldav/upgrade.py	2010-01-25 15:56:33 UTC (rev 4958)
@@ -1,6 +1,6 @@
 # -*- test-case-name: twistedcaldav.test.test_upgrade -*-
 ##
-# Copyright (c) 2008 Apple Inc. All rights reserved.
+# Copyright (c) 2008-2010 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.
@@ -20,7 +20,6 @@
 from twisted.web2.dav.fileop import rmdir
 from twisted.web2.dav import davxml
 from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
 from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
 from twistedcaldav.mail import MailGatewayTokensDatabase
 from twistedcaldav.log import Logger
@@ -208,44 +207,44 @@
 
 
     def doProxyDatabaseMoveUpgrade(config, uid=-1, gid=-1):
+        pass
+#        # See if the new one is already present
+#        newDbPath = os.path.join(config.DataRoot,
+#            CalendarUserProxyDatabase.dbFilename)
+#        if os.path.exists(newDbPath):
+#            # Nothing to be done, it's already in the new location
+#            return
+#
+#        # See if the old DB is present
+#        oldDbPath = os.path.join(config.DocumentRoot, "principals",
+#            CalendarUserProxyDatabase.dbOldFilename)
+#        if not os.path.exists(oldDbPath):
+#            # Nothing to be moved
+#            return
+#
+#        # Now move the old one to the new location
+#        try:
+#            if not os.path.exists(config.DataRoot):
+#                makeDirsUserGroup(config.DataRoot, uid=uid, gid=gid)
+#            try:
+#                os.rename(oldDbPath, newDbPath)
+#            except OSError:
+#                # Can't rename, must copy/delete
+#                shutil.copy2(oldDbPath, newDbPath)
+#                os.remove(oldDbPath)
+#
+#        except Exception, e:
+#            raise UpgradeError(
+#                "Upgrade Error: unable to move the old calendar user proxy database at '%s' to '%s' due to %s."
+#                % (oldDbPath, newDbPath, str(e))
+#            )
+#
+#        log.debug(
+#            "Moved the calendar user proxy database from '%s' to '%s'."
+#            % (oldDbPath, newDbPath,)
+#        )
 
-        # See if the new one is already present
-        newDbPath = os.path.join(config.DataRoot,
-            CalendarUserProxyDatabase.dbFilename)
-        if os.path.exists(newDbPath):
-            # Nothing to be done, it's already in the new location
-            return
 
-        # See if the old DB is present
-        oldDbPath = os.path.join(config.DocumentRoot, "principals",
-            CalendarUserProxyDatabase.dbOldFilename)
-        if not os.path.exists(oldDbPath):
-            # Nothing to be moved
-            return
-
-        # Now move the old one to the new location
-        try:
-            if not os.path.exists(config.DataRoot):
-                makeDirsUserGroup(config.DataRoot, uid=uid, gid=gid)
-            try:
-                os.rename(oldDbPath, newDbPath)
-            except OSError:
-                # Can't rename, must copy/delete
-                shutil.copy2(oldDbPath, newDbPath)
-                os.remove(oldDbPath)
-
-        except Exception, e:
-            raise UpgradeError(
-                "Upgrade Error: unable to move the old calendar user proxy database at '%s' to '%s' due to %s."
-                % (oldDbPath, newDbPath, str(e))
-            )
-
-        log.debug(
-            "Moved the calendar user proxy database from '%s' to '%s'."
-            % (oldDbPath, newDbPath,)
-        )
-
-
     def moveCalendarHome(oldHome, newHome, uid=-1, gid=-1):
         if os.path.exists(newHome):
             # Both old and new homes exist; stop immediately to let the
@@ -261,31 +260,35 @@
 
 
     def migrateResourceInfo(config, directory, uid, gid):
-        log.info("Fetching delegate assignments and auto-schedule settings from directory")
-        resourceInfoDatabase = ResourceInfoDatabase(config.DataRoot)
-        calendarUserProxyDatabase = CalendarUserProxyDatabase(config.DataRoot)
-        resourceInfo = directory.getResourceInfo()
-        for guid, autoSchedule, proxy, readOnlyProxy in resourceInfo:
-            resourceInfoDatabase.setAutoScheduleInDatabase(guid, autoSchedule)
-            if proxy:
-                calendarUserProxyDatabase.setGroupMembersInDatabase(
-                    "%s#calendar-proxy-write" % (guid,),
-                    [proxy]
-                )
-            if readOnlyProxy:
-                calendarUserProxyDatabase.setGroupMembersInDatabase(
-                    "%s#calendar-proxy-read" % (guid,),
-                    [readOnlyProxy]
-                )
+        # TODO: we need to account for the new augments database. This means migrating from the pre-resource info
+        # implementation and the resource-info implementation
+        pass
 
-        dbPath = os.path.join(config.DataRoot, ResourceInfoDatabase.dbFilename)
-        if os.path.exists(dbPath):
-            os.chown(dbPath, uid, gid)
+#        log.info("Fetching delegate assignments and auto-schedule settings from directory")
+#        resourceInfoDatabase = ResourceInfoDatabase(config.DataRoot)
+#        calendarUserProxyDatabase = CalendarUserProxyDatabase(config.DataRoot)
+#        resourceInfo = directory.getResourceInfo()
+#        for guid, autoSchedule, proxy, readOnlyProxy in resourceInfo:
+#            resourceInfoDatabase.setAutoScheduleInDatabase(guid, autoSchedule)
+#            if proxy:
+#                calendarUserProxyDatabase.setGroupMembersInDatabase(
+#                    "%s#calendar-proxy-write" % (guid,),
+#                    [proxy]
+#                )
+#            if readOnlyProxy:
+#                calendarUserProxyDatabase.setGroupMembersInDatabase(
+#                    "%s#calendar-proxy-read" % (guid,),
+#                    [readOnlyProxy]
+#                )
+#
+#        dbPath = os.path.join(config.DataRoot, ResourceInfoDatabase.dbFilename)
+#        if os.path.exists(dbPath):
+#            os.chown(dbPath, uid, gid)
+#
+#        dbPath = os.path.join(config.DataRoot, CalendarUserProxyDatabase.dbFilename)
+#        if os.path.exists(dbPath):
+#            os.chown(dbPath, uid, gid)
 
-        dbPath = os.path.join(config.DataRoot, CalendarUserProxyDatabase.dbFilename)
-        if os.path.exists(dbPath):
-            os.chown(dbPath, uid, gid)
-
     def createMailTokensDatabase(config, uid, gid):
         # Cause the tokens db to be created on disk so we can set the
         # permissions on it now
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100125/fda01adc/attachment-0001.html>


More information about the calendarserver-changes mailing list