[CalendarServer-changes] [14084] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Oct 16 09:12:30 PDT 2014


Revision: 14084
          http://trac.calendarserver.org//changeset/14084
Author:   cdaboo at apple.com
Date:     2014-10-16 09:12:30 -0700 (Thu, 16 Oct 2014)
Log Message:
-----------
Cross-pod proxy support. Still needs work to better handle errors and improve performance.

Modified Paths:
--------------
    CalendarServer/trunk/conf/caldavd-test-podA.plist
    CalendarServer/trunk/conf/caldavd-test-podB.plist
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_localservers.py
    CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
    CalendarServer/trunk/txdav/who/cache.py
    CalendarServer/trunk/txdav/who/delegates.py

Added Paths:
-----------
    CalendarServer/trunk/bin/testpods
    CalendarServer/trunk/txdav/common/datastore/podding/attachments.py
    CalendarServer/trunk/txdav/common/datastore/podding/base.py
    CalendarServer/trunk/txdav/common/datastore/podding/directory.py
    CalendarServer/trunk/txdav/common/datastore/podding/sharing_base.py
    CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py
    CalendarServer/trunk/txdav/common/datastore/podding/sharing_store.py

Added: CalendarServer/trunk/bin/testpods
===================================================================
--- CalendarServer/trunk/bin/testpods	                        (rev 0)
+++ CalendarServer/trunk/bin/testpods	2014-10-16 16:12:30 UTC (rev 14084)
@@ -0,0 +1,173 @@
+#!/bin/sh
+# -*- sh-basic-offset: 2 -*-
+
+##
+# Copyright (c) 2005-2014 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.
+##
+
+set -e;
+set -u;
+
+wd="$(cd "$(dirname "$0")/.." && pwd -L)";
+
+. "${wd}/bin/_build.sh";
+
+init_build > /dev/null;
+
+cdt="${py_virtualenv}/src/caldavtester";
+
+##
+# Command line handling
+##
+
+   verbose="";
+serverinfo="${cdt}/scripts/server/serverinfo-pod.xml";
+  printres="";
+    subdir="";
+    random="--random";
+      seed="";
+       ssl="";
+  cdtdebug="";
+
+usage ()
+{
+  program="$(basename "$0")";
+  echo "Usage: ${program} [-v] [-s serverinfo]";
+  echo "Options:";
+  echo "        -d  Set the script subdirectory";
+  echo "        -h  Print this help and exit";
+  echo "        -o  Execute tests in order";
+  echo "        -r  Print request and response";
+  echo "        -s  Set the serverinfo.xml";
+  echo "        -t  Set the CalDAVTester directory";
+  echo "        -x  Random seed to use.";
+  echo "        -v  Verbose.";
+  echo "        -z  Use SSL.";
+  echo "        -D  Turn on CalDAVTester debugging";
+
+  if [ "${1-}" == "-" ]; then return 0; fi;
+  exit 64;
+}
+
+while getopts 'Dhvrozt:s:d:x:' option; do
+  case "$option" in
+    '?') usage; ;;
+    'h') usage -; exit 0; ;;
+    't') cdt="${OPTARG}"; serverinfo="${OPTARG}/scripts/server/serverinfo-pod.xml"; ;;
+    'd') subdir="--subdir=${OPTARG}"; ;;
+    's') serverinfo="${OPTARG}"; ;;
+    'r') printres="--always-print-request --always-print-response"; ;;
+    'v') verbose="v"; ;;
+    'o') random=""; ;;
+    'x') seed="--random-seed ${OPTARG}"; ;;
+    'z') ssl="--ssl"; ;;
+    'D') cdtdebug="--debug"; ;;
+  esac;
+done;
+
+shift $((${OPTIND} - 1));
+
+if [ $# == 0 ]; then
+  set - "--all";
+fi;
+
+##
+# Do The Right Thing
+##
+
+do_setup="false";
+develop > /dev/null;
+
+# Set up sandbox
+
+sandboxdir="/tmp/cdt_server_sandbox"
+
+if [ -d "${sandboxdir}" ]; then
+  rm -rf "${sandboxdir}"
+fi;
+
+configdir="${sandboxdir}/Config"
+serverrootA="${sandboxdir}/podA"
+serverrootB="${sandboxdir}/podB"
+
+mkdir -p "${configdir}/auth"
+mkdir -p "${serverrootA}/Logs" "${serverrootA}/Run" "${serverrootA}/Data/Documents"
+mkdir -p "${serverrootB}/Logs" "${serverrootB}/Run" "${serverrootB}/Data/Documents"
+
+cp conf/caldavd-test.plist "${configdir}/caldavd-cdt.plist"
+cp conf/caldavd-test-podA.plist "${configdir}/caldavd-cdt-podA.plist"
+cp conf/caldavd-test-podB.plist "${configdir}/caldavd-cdt-podB.plist"
+cp conf/auth/proxies-test-pod.xml "${configdir}/auth/proxies-cdt.xml"
+cp conf/auth/resources-test-pod.xml "${configdir}/auth/resources-cdt.xml"
+cp conf/auth/augments-test-pod.xml "${configdir}/auth/augments-cdt.xml"
+cp conf/auth/accounts-test-pod.xml "${configdir}/auth/accounts-cdt.xml"
+
+# Modify the plists
+
+python -c "import plistlib; f=plistlib.readPlist('${configdir}/caldavd-cdt.plist'); f['ConfigRoot'] = '${configdir}'; f['RunRoot'] = 'Run'; f['Authentication']['Kerberos']['Enabled'] = False; plistlib.writePlist(f, '${configdir}/caldavd-cdt.plist');"
+python -c "import plistlib; f=plistlib.readPlist('${configdir}/caldavd-cdt-podA.plist'); f['ImportConfig'] = '${configdir}/caldavd-cdt.plist'; f['ServerRoot'] = '${serverrootA}'; f['ProxyLoadFromFile'] = '${configdir}/auth/proxies-cdt.xml'; f['ResourceService']['params']['xmlFile'] = '${configdir}/auth/resources-cdt.xml'; f['DirectoryService']['params']['xmlFile'] = '${configdir}/auth/accounts-cdt.xml'; f['AugmentService']['params']['xmlFiles'] = ['${configdir}/auth/augments-cdt.xml']; plistlib.writePlist(f, '${configdir}/caldavd-cdt-podA.plist');"
+python -c "import plistlib; f=plistlib.readPlist('${configdir}/caldavd-cdt-podB.plist'); f['ImportConfig'] = '${configdir}/caldavd-cdt.plist'; f['ServerRoot'] = '${serverrootB}'; f['ProxyLoadFromFile'] = '${configdir}/auth/proxies-cdt.xml'; f['ResourceService']['params']['xmlFile'] = '${configdir}/auth/resources-cdt.xml'; f['DirectoryService']['params']['xmlFile'] = '${configdir}/auth/accounts-cdt.xml'; f['AugmentService']['params']['xmlFiles'] = ['${configdir}/auth/augments-cdt.xml']; plistlib.writePlist(f, '${configdir}/caldavd-cdt-podB.plist');"
+
+runpod() {
+	local podsuffix="$1"; shift;
+
+	# Start the server
+	
+	"${wd}/bin/run" -nd -c "${configdir}/caldavd-cdt-${podsuffix}.plist"
+	
+	/bin/echo -n "Waiting for server ${podsuffix} to start up..."
+	
+	while [ ! -f "${sandboxdir}/${podsuffix}/Run/caldav-instance-0.pid" ]; do
+	  sleep 1
+	  /bin/echo -n "."
+	done;
+	
+	echo "Server ${podsuffix} has started"
+}
+
+stoppod() {
+	local podsuffix="$1"; shift;
+
+	echo "Stopping server ${podsuffix}"
+	"${wd}/bin/run" -nk -c "${configdir}/caldavd-cdt-${podsuffix}.plist"
+}
+
+runpod "podA";
+runpod "podB";
+
+# Don't exit if testcaldav.py fails, because we need to clean up afterwards.
+
+set +e
+
+# Run CDT
+
+echo "Starting CDT run"
+
+cd "${cdt}" && "${python}" testcaldav.py ${random} ${seed} ${ssl} ${cdtdebug} --print-details-onfail ${printres} -s "${serverinfo}" -x scripts/tests-pod ${subdir} "$@";
+
+# Capture exit status of testcaldav.py to use as this script's exit status.
+
+STATUS=$?
+
+# Re-enable exit on failure incase run -nk fails
+
+set -e
+
+stoppod "podA";
+stoppod "podB";
+
+# Exit with the exit status of testcaldav.py, to reflect the test suite's result
+
+exit $STATUS


Property changes on: CalendarServer/trunk/bin/testpods
___________________________________________________________________
Added: svn:executable
   + *

Modified: CalendarServer/trunk/conf/caldavd-test-podA.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test-podA.plist	2014-10-16 16:09:16 UTC (rev 14083)
+++ CalendarServer/trunk/conf/caldavd-test-podA.plist	2014-10-16 16:12:30 UTC (rev 14084)
@@ -55,7 +55,7 @@
     <key>DirectoryService</key>
     <dict>
       <key>type</key>
-      <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
+      <string>xml</string>
       
       <key>params</key>
       <dict>
@@ -70,7 +70,7 @@
       <key>Enabled</key>
       <true/>
       <key>type</key>
-      <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
+      <string>xml</string>
       
       <key>params</key>
       <dict>
@@ -202,8 +202,6 @@
 		</dict>
       <key>MaxClients</key>
       <integer>5</integer>
-      <key>memcached</key>
-      <string>../memcached/_root/bin/memcached</string> <!-- Find in PATH -->
       <key>Options</key>
       <array>
         <!--<string>-vv</string>-->

Modified: CalendarServer/trunk/conf/caldavd-test-podB.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test-podB.plist	2014-10-16 16:09:16 UTC (rev 14083)
+++ CalendarServer/trunk/conf/caldavd-test-podB.plist	2014-10-16 16:12:30 UTC (rev 14084)
@@ -62,7 +62,7 @@
     <key>DirectoryService</key>
     <dict>
       <key>type</key>
-      <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
+      <string>xml</string>
       
       <key>params</key>
       <dict>
@@ -77,7 +77,7 @@
       <key>Enabled</key>
       <true/>
       <key>type</key>
-      <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
+      <string>xml</string>
       
       <key>params</key>
       <dict>
@@ -101,19 +101,6 @@
       </dict>
     </dict>
 
-    <!-- Sqlite ProxyDB Service - must use the same one as Pod A-->
-    <key>ProxyDBService</key>
-    <dict>
-      <key>type</key>
-      <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
-      
-      <key>params</key>
-      <dict>
-        <key>dbpath</key>
-        <string>./data/podA/Data/proxies.sqlite</string>
-      </dict>
-    </dict>
-
     <key>ProxyLoadFromFile</key>
     <string>./conf/auth/proxies-test-pod.xml</string>
 
@@ -222,8 +209,6 @@
 		</dict>
       <key>MaxClients</key>
       <integer>5</integer>
-      <key>memcached</key>
-      <string>../memcached/_root/bin/memcached</string> <!-- Find in PATH -->
       <key>Options</key>
       <array>
         <!--<string>-vv</string>-->

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py	2014-10-16 16:09:16 UTC (rev 14083)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/localservers.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -105,6 +105,10 @@
         return self._thisServer
 
 
+    def allServersExceptThis(self):
+        return filter(lambda x: x != self._thisServer, self._servers.values())
+
+
     def installReverseProxies(self, maxClients):
         """
         Install a reverse proxy for each of the other servers in the "pod".

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_localservers.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_localservers.py	2014-10-16 16:09:16 UTC (rev 14083)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/ischedule/test/test_localservers.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -93,6 +93,7 @@
 
         self.assertTrue(servers.getServerById("00001").thisServer)
         self.assertFalse(servers.getServerById("00002").thisServer)
+        self.assertEqual(servers.getThisServer(), servers.getServerById("00001"))
 
         self.patch(config, "ServerHostName", "caldav2.example.com")
         self.patch(config, "SSLPort", 8443)
@@ -104,8 +105,30 @@
 
         self.assertFalse(servers.getServerById("00001").thisServer)
         self.assertTrue(servers.getServerById("00002").thisServer)
+        self.assertEqual(servers.getThisServer(), servers.getServerById("00002"))
 
 
+    def test_all_except_this_server(self):
+
+        servers = self._setupServers()
+
+        self.assertTrue(servers.getServerById("00001").thisServer)
+        self.assertFalse(servers.getServerById("00002").thisServer)
+        self.assertEqual(servers.allServersExceptThis(), [servers.getServerById("00002"), ])
+
+        self.patch(config, "ServerHostName", "caldav2.example.com")
+        self.patch(config, "SSLPort", 8443)
+        self.patch(config, "BindSSLPorts", [8843])
+
+        xmlFile = StringIO.StringIO(ServerTests.data1)
+        servers = ServersDB()
+        servers.load(xmlFile, ignoreIPLookupFailures=True)
+
+        self.assertFalse(servers.getServerById("00001").thisServer)
+        self.assertTrue(servers.getServerById("00002").thisServer)
+        self.assertEqual(servers.allServersExceptThis(), [servers.getServerById("00001"), ])
+
+
     def test_check_this_ip(self):
 
         servers = self._setupServers()

Added: CalendarServer/trunk/txdav/common/datastore/podding/attachments.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/attachments.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/attachments.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -0,0 +1,204 @@
+##
+# Copyright (c) 2013-2014 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
+from twisted.python.reflect import namedClass
+
+
+class AttachmentsPoddingConduitMixin(object):
+    """
+    Defines the cross-pod API for managed attachments that will be mixed into the
+    L{PoddingConduit} class.
+    """
+
+    @inlineCallbacks
+    def send_add_attachment(self, objectResource, rids, content_type, filename, stream):
+        """
+        Managed attachment addAttachment call.
+
+        @param objectResource: child resource having an attachment added
+        @type objectResource: L{CalendarObject}
+        @param rids: list of recurrence ids
+        @type rids: C{list}
+        @param content_type: content type of attachment data
+        @type content_type: L{MimeType}
+        @param filename: name of attachment
+        @type filename: C{str}
+        @param stream: attachment data stream
+        @type stream: L{IStream}
+        """
+
+        actionName = "add-attachment"
+        shareeView = objectResource._parentCollection
+        request, recipient = yield self._getRequestForResource(actionName, shareeView, objectResource)
+        request["rids"] = rids
+        request["filename"] = filename
+
+        response = yield self.sendRequest(shareeView._txn, recipient, request, stream, content_type)
+
+        if response["result"] == "ok":
+            returnValue(response["value"])
+        elif response["result"] == "exception":
+            raise namedClass(response["class"])(response["result"])
+
+
+    @inlineCallbacks
+    def recv_add_attachment(self, txn, request):
+        """
+        Process an addAttachment cross-pod request. Request arguments as per L{send_add_attachment}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        actionName = "add-attachment"
+        _ignore_shareeView, objectResource = yield self._getResourcesForRequest(txn, request, actionName)
+        try:
+            attachment, location = yield objectResource.addAttachment(
+                request["rids"],
+                request["streamType"],
+                request["filename"],
+                request["stream"],
+            )
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "request": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": (attachment.managedID(), location,),
+        })
+
+
+    @inlineCallbacks
+    def send_update_attachment(self, objectResource, managed_id, content_type, filename, stream):
+        """
+        Managed attachment updateAttachment call.
+
+        @param objectResource: child resource having an attachment added
+        @type objectResource: L{CalendarObject}
+        @param managed_id: managed-id to update
+        @type managed_id: C{str}
+        @param content_type: content type of attachment data
+        @type content_type: L{MimeType}
+        @param filename: name of attachment
+        @type filename: C{str}
+        @param stream: attachment data stream
+        @type stream: L{IStream}
+        """
+
+        actionName = "update-attachment"
+        shareeView = objectResource._parentCollection
+        request, recipient = yield self._getRequestForResource(actionName, shareeView, objectResource)
+        request["managedID"] = managed_id
+        request["filename"] = filename
+
+        response = yield self.sendRequest(shareeView._txn, recipient, request, stream, content_type)
+
+        if response["result"] == "ok":
+            returnValue(response["value"])
+        elif response["result"] == "exception":
+            raise namedClass(response["class"])(response["result"])
+
+
+    @inlineCallbacks
+    def recv_update_attachment(self, txn, request):
+        """
+        Process an updateAttachment cross-pod request. Request arguments as per L{send_update_attachment}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        actionName = "update-attachment"
+        _ignore_shareeView, objectResource = yield self._getResourcesForRequest(txn, request, actionName)
+        try:
+            attachment, location = yield objectResource.updateAttachment(
+                request["managedID"],
+                request["streamType"],
+                request["filename"],
+                request["stream"],
+            )
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "request": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": (attachment.managedID(), location,),
+        })
+
+
+    @inlineCallbacks
+    def send_remove_attachment(self, objectResource, rids, managed_id):
+        """
+        Managed attachment removeAttachment call.
+
+        @param objectResource: child resource having an attachment added
+        @type objectResource: L{CalendarObject}
+        @param rids: list of recurrence ids
+        @type rids: C{list}
+        @param managed_id: managed-id to update
+        @type managed_id: C{str}
+        """
+
+        actionName = "remove-attachment"
+        shareeView = objectResource._parentCollection
+        request, recipient = yield self._getRequestForResource(actionName, shareeView, objectResource)
+        request["rids"] = rids
+        request["managedID"] = managed_id
+
+        response = yield self.sendRequest(shareeView._txn, recipient, request)
+
+        if response["result"] == "ok":
+            returnValue(response["value"])
+        elif response["result"] == "exception":
+            raise namedClass(response["class"])(response["result"])
+
+
+    @inlineCallbacks
+    def recv_remove_attachment(self, txn, request):
+        """
+        Process an removeAttachment cross-pod request. Request arguments as per L{send_remove_attachment}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        actionName = "remove-attachment"
+        _ignore_shareeView, objectResource = yield self._getResourcesForRequest(txn, request, actionName)
+        try:
+            yield objectResource.removeAttachment(
+                request["rids"],
+                request["managedID"],
+            )
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "request": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": None,
+        })

Added: CalendarServer/trunk/txdav/common/datastore/podding/base.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/base.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/base.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -0,0 +1,21 @@
+##
+# Copyright (c) 2013-2014 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.
+##
+
+class FailedCrossPodRequestError(RuntimeError):
+    """
+    Request returned an error.
+    """
+    pass

Modified: CalendarServer/trunk/txdav/common/datastore/podding/conduit.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/conduit.py	2014-10-16 16:09:16 UTC (rev 14083)
+++ CalendarServer/trunk/txdav/common/datastore/podding/conduit.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -17,29 +17,26 @@
 from twext.python.log import Logger
 
 from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.python.reflect import namedClass
 
-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
-from txdav.common.datastore.podding.request import ConduitRequest
 from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
-from txdav.common.icommondatastore import ExternalShareFailed
+from txdav.common.datastore.podding.attachments import AttachmentsPoddingConduitMixin
+from txdav.common.datastore.podding.base import FailedCrossPodRequestError
+from txdav.common.datastore.podding.directory import DirectoryPoddingConduitMixin
+from txdav.common.datastore.podding.request import ConduitRequest
+from txdav.common.datastore.podding.sharing_invites import SharingInvitesPoddingConduitMixin
+from txdav.common.datastore.podding.sharing_store import SharingStorePoddingConduitMixin
 
-from twistedcaldav.caldavxml import TimeRange
 
-
 log = Logger()
 
 
-class FailedCrossPodRequestError(RuntimeError):
+class PoddingConduit(
+    AttachmentsPoddingConduitMixin,
+    SharingInvitesPoddingConduitMixin,
+    SharingStorePoddingConduitMixin,
+    DirectoryPoddingConduitMixin,
+):
     """
-    Request returned an error.
-    """
-    pass
-
-
-
-class PoddingConduit(object):
-    """
     This class is the API/RPC bridge between cross-pod requests and the store.
 
     Each cross-pod request/response is described by a Python C{dict} that is serialized
@@ -63,8 +60,8 @@
 
     Some simple forms of send_/recv_ methods can be auto-generated to simplify coding.
 
-    Right now this conduit is used for cross-pod sharing operations. In the future we will
-    likely use it for cross-pod migration.
+    Actual implementations of this will be done via mix-ins for the different sub-systems using
+    the conduit.
     """
 
     conduitRequestClass = ConduitRequest
@@ -105,10 +102,14 @@
         returnValue((source, destination,))
 
 
-    @inlineCallbacks
     def sendRequest(self, txn, recipient, data, stream=None, streamType=None):
+        return self.sendRequestToServer(txn, recipient.server(), data, stream, streamType)
 
-        request = self.conduitRequestClass(recipient.server(), data, stream, streamType)
+
+    @inlineCallbacks
+    def sendRequestToServer(self, txn, server, data, stream=None, streamType=None):
+
+        request = self.conduitRequestClass(server, data, stream, streamType)
         try:
             response = (yield request.doRequest(txn))
         except Exception as e:
@@ -154,691 +155,3 @@
         yield txn.commit()
 
         returnValue(result)
-
-
-    #
-    # Invite related apis
-    #
-
-    @inlineCallbacks
-    def send_shareinvite(self, txn, homeType, ownerUID, ownerID, ownerName, shareeUID, shareUID, bindMode, summary, copy_properties, supported_components):
-        """
-        Send a sharing invite cross-pod message.
-
-        @param homeType: Type of home being shared.
-        @type homeType: C{int}
-        @param ownerUID: UID of the sharer.
-        @type ownerUID: C{str}
-        @param ownerID: resource ID of the sharer calendar
-        @type ownerID: C{int}
-        @param ownerName: owner's name of the sharer calendar
-        @type ownerName: C{str}
-        @param shareeUID: UID of the sharee
-        @type shareeUID: C{str}
-        @param shareUID: Resource/invite ID for sharee
-        @type shareUID: C{str}
-        @param bindMode: bind mode for the share
-        @type bindMode: C{str}
-        @param summary: sharing message
-        @type summary: C{str}
-        @param copy_properties: C{str} name/value for properties to be copied
-        @type copy_properties: C{dict}
-        @param supported_components: supproted components, may be C{None}
-        @type supported_components: C{str}
-        """
-
-        _ignore_sender, recipient = yield self.validRequest(ownerUID, shareeUID)
-
-        action = {
-            "action": "shareinvite",
-            "type": homeType,
-            "owner": ownerUID,
-            "owner_id": ownerID,
-            "owner_name": ownerName,
-            "sharee": shareeUID,
-            "share_id": shareUID,
-            "mode": bindMode,
-            "summary": summary,
-            "properties": copy_properties,
-        }
-        if supported_components is not None:
-            action["supported-components"] = supported_components
-
-        result = yield self.sendRequest(txn, recipient, action)
-        returnValue(result)
-
-
-    @inlineCallbacks
-    def recv_shareinvite(self, txn, message):
-        """
-        Process a sharing invite cross-pod message. Message arguments as per L{send_shareinvite}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        if message["action"] != "shareinvite":
-            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareinvite".format(message["action"]))
-
-        # Create a share
-        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
-        if shareeHome is None or shareeHome.external():
-            raise FailedCrossPodRequestError("Invalid sharee UID specified")
-
-        try:
-            yield shareeHome.processExternalInvite(
-                message["owner"],
-                message["owner_id"],
-                message["owner_name"],
-                message["share_id"],
-                message["mode"],
-                message["summary"],
-                message["properties"],
-                supported_components=message.get("supported-components")
-            )
-        except ExternalShareFailed as e:
-            raise FailedCrossPodRequestError(str(e))
-
-        returnValue({
-            "result": "ok",
-        })
-
-
-    @inlineCallbacks
-    def send_shareuninvite(self, txn, homeType, ownerUID, ownerID, shareeUID, shareUID):
-        """
-        Send a sharing uninvite cross-pod message.
-
-        @param homeType: Type of home being shared.
-        @type homeType: C{int}
-        @param ownerUID: UID of the sharer.
-        @type ownerUID: C{str}
-        @param ownerID: resource ID of the sharer calendar
-        @type ownerID: C{int}
-        @param shareeUID: UID of the sharee
-        @type shareeUID: C{str}
-        @param shareUID: Resource/invite ID for sharee
-        @type shareUID: C{str}
-        """
-
-        _ignore_sender, recipient = yield self.validRequest(ownerUID, shareeUID)
-
-        action = {
-            "action": "shareuninvite",
-            "type": homeType,
-            "owner": ownerUID,
-            "owner_id": ownerID,
-            "sharee": shareeUID,
-            "share_id": shareUID,
-        }
-
-        result = yield self.sendRequest(txn, recipient, action)
-        returnValue(result)
-
-
-    @inlineCallbacks
-    def recv_shareuninvite(self, txn, message):
-        """
-        Process a sharing uninvite cross-pod message. Message arguments as per L{send_shareuninvite}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        if message["action"] != "shareuninvite":
-            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareuninvite".format(message["action"]))
-
-        # Create a share
-        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
-        if shareeHome is None or shareeHome.external():
-            FailedCrossPodRequestError("Invalid sharee UID specified")
-
-        try:
-            yield shareeHome.processExternalUninvite(
-                message["owner"],
-                message["owner_id"],
-                message["share_id"],
-            )
-        except ExternalShareFailed as e:
-            FailedCrossPodRequestError(str(e))
-
-        returnValue({
-            "result": "ok",
-        })
-
-
-    @inlineCallbacks
-    def send_sharereply(self, txn, homeType, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
-        """
-        Send a sharing reply cross-pod message.
-
-        @param homeType: Type of home being shared.
-        @type homeType: C{int}
-        @param ownerUID: UID of the sharer.
-        @type ownerUID: C{str}
-        @param shareeUID: UID of the recipient
-        @type shareeUID: C{str}
-        @param shareUID: Resource/invite ID for recipient
-        @type shareUID: C{str}
-        @param bindStatus: bind mode for the share
-        @type bindStatus: C{str}
-        @param summary: sharing message
-        @type summary: C{str}
-        """
-
-        _ignore_sender, recipient = yield self.validRequest(shareeUID, ownerUID)
-
-        action = {
-            "action": "sharereply",
-            "type": homeType,
-            "owner": ownerUID,
-            "sharee": shareeUID,
-            "share_id": shareUID,
-            "status": bindStatus,
-        }
-        if summary is not None:
-            action["summary"] = summary
-
-        result = yield self.sendRequest(txn, recipient, action)
-        returnValue(result)
-
-
-    @inlineCallbacks
-    def recv_sharereply(self, txn, message):
-        """
-        Process a sharing reply cross-pod message. Message arguments as per L{send_sharereply}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        if message["action"] != "sharereply":
-            raise FailedCrossPodRequestError("Wrong action '{}' for recv_sharereply".format(message["action"]))
-
-        # Create a share
-        ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
-        if ownerHome is None or ownerHome.external():
-            FailedCrossPodRequestError("Invalid owner UID specified")
-
-        try:
-            yield ownerHome.processExternalReply(
-                message["owner"],
-                message["sharee"],
-                message["share_id"],
-                message["status"],
-                summary=message.get("summary")
-            )
-        except ExternalShareFailed as e:
-            FailedCrossPodRequestError(str(e))
-
-        returnValue({
-            "result": "ok",
-        })
-
-
-    #
-    # Managed attachment related apis
-    #
-
-    @inlineCallbacks
-    def send_add_attachment(self, objectResource, rids, content_type, filename, stream):
-        """
-        Managed attachment addAttachment call.
-
-        @param objectResource: child resource having an attachment added
-        @type objectResource: L{CalendarObject}
-        @param rids: list of recurrence ids
-        @type rids: C{list}
-        @param content_type: content type of attachment data
-        @type content_type: L{MimeType}
-        @param filename: name of attachment
-        @type filename: C{str}
-        @param stream: attachment data stream
-        @type stream: L{IStream}
-        """
-
-        actionName = "add-attachment"
-        shareeView = objectResource._parentCollection
-        action, recipient = yield self._send(actionName, shareeView, objectResource)
-        action["rids"] = rids
-        action["filename"] = filename
-        result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
-        if result["result"] == "ok":
-            returnValue(result["value"])
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def recv_add_attachment(self, txn, message):
-        """
-        Process an addAttachment cross-pod message. Message arguments as per L{send_add_attachment}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        actionName = "add-attachment"
-        _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
-        try:
-            attachment, location = yield objectResource.addAttachment(
-                message["rids"],
-                message["streamType"],
-                message["filename"],
-                message["stream"],
-            )
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        returnValue({
-            "result": "ok",
-            "value": (attachment.managedID(), location,),
-        })
-
-
-    @inlineCallbacks
-    def send_update_attachment(self, objectResource, managed_id, content_type, filename, stream):
-        """
-        Managed attachment updateAttachment call.
-
-        @param objectResource: child resource having an attachment added
-        @type objectResource: L{CalendarObject}
-        @param managed_id: managed-id to update
-        @type managed_id: C{str}
-        @param content_type: content type of attachment data
-        @type content_type: L{MimeType}
-        @param filename: name of attachment
-        @type filename: C{str}
-        @param stream: attachment data stream
-        @type stream: L{IStream}
-        """
-
-        actionName = "update-attachment"
-        shareeView = objectResource._parentCollection
-        action, recipient = yield self._send(actionName, shareeView, objectResource)
-        action["managedID"] = managed_id
-        action["filename"] = filename
-        result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
-        if result["result"] == "ok":
-            returnValue(result["value"])
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def recv_update_attachment(self, txn, message):
-        """
-        Process an updateAttachment cross-pod message. Message arguments as per L{send_update_attachment}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        actionName = "update-attachment"
-        _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
-        try:
-            attachment, location = yield objectResource.updateAttachment(
-                message["managedID"],
-                message["streamType"],
-                message["filename"],
-                message["stream"],
-            )
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        returnValue({
-            "result": "ok",
-            "value": (attachment.managedID(), location,),
-        })
-
-
-    @inlineCallbacks
-    def send_remove_attachment(self, objectResource, rids, managed_id):
-        """
-        Managed attachment removeAttachment call.
-
-        @param objectResource: child resource having an attachment added
-        @type objectResource: L{CalendarObject}
-        @param rids: list of recurrence ids
-        @type rids: C{list}
-        @param managed_id: managed-id to update
-        @type managed_id: C{str}
-        """
-
-        actionName = "remove-attachment"
-        shareeView = objectResource._parentCollection
-        action, recipient = yield self._send(actionName, shareeView, objectResource)
-        action["rids"] = rids
-        action["managedID"] = managed_id
-        result = yield self.sendRequest(shareeView._txn, recipient, action)
-        if result["result"] == "ok":
-            returnValue(result["value"])
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def recv_remove_attachment(self, txn, message):
-        """
-        Process an removeAttachment cross-pod message. Message arguments as per L{send_remove_attachment}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        actionName = "remove-attachment"
-        _ignore_shareeView, objectResource = yield self._recv(txn, message, actionName)
-        try:
-            yield objectResource.removeAttachment(
-                message["rids"],
-                message["managedID"],
-            )
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        returnValue({
-            "result": "ok",
-            "value": None,
-        })
-
-
-    #
-    # Sharer data access related apis
-    #
-
-    @inlineCallbacks
-    def _send(self, action, parent, child=None):
-        """
-        Base behavior for an operation on a L{CommonHomeChild}.
-
-        @param shareeView: sharee resource being operated on.
-        @type shareeView: L{CommonHomeChildExternal}
-        """
-
-        homeType = parent.ownerHome()._homeType
-        ownerUID = parent.ownerHome().uid()
-        ownerID = parent.external_id()
-        shareeUID = parent.viewerHome().uid()
-
-        _ignore_sender, recipient = yield self.validRequest(shareeUID, ownerUID)
-
-        result = {
-            "action": action,
-            "type": homeType,
-            "owner": ownerUID,
-            "owner_id": ownerID,
-            "sharee": shareeUID,
-        }
-        if child is not None:
-            result["resource_id"] = child.id()
-        returnValue((result, recipient))
-
-
-    @inlineCallbacks
-    def _recv(self, txn, message, expected_action):
-        """
-        Base behavior for sharer data access.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        if message["action"] != expected_action:
-            raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(message["action"], expected_action))
-
-        # Get a share
-        ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
-        if ownerHome is None or ownerHome.external():
-            FailedCrossPodRequestError("Invalid owner UID specified")
-
-        shareeHome = yield txn.homeWithUID(message["type"], message["sharee"])
-        if shareeHome is None or not shareeHome.external():
-            FailedCrossPodRequestError("Invalid sharee UID specified")
-
-        shareeView = yield shareeHome.childWithID(message["owner_id"])
-        if shareeView is None:
-            FailedCrossPodRequestError("Invalid shared resource specified")
-
-        resourceID = message.get("resource_id", None)
-        if resourceID is not None:
-            objectResource = yield shareeView.objectResourceWithID(resourceID)
-            if objectResource is None:
-                FailedCrossPodRequestError("Invalid owner shared object resource specified")
-        else:
-            objectResource = None
-
-        returnValue((shareeView, objectResource,))
-
-
-    #
-    # Simple calls are ones where there is no argument and a single return value. We can simplify
-    # code generation for these by dynamically generating the appropriate class methods.
-    #
-
-    @inlineCallbacks
-    def _simple_send(self, actionName, shareeView, objectResource=None, transform=None, args=None, kwargs=None):
-        """
-        A simple send operation that returns a value.
-
-        @param actionName: name of the action.
-        @type actionName: C{str}
-        @param shareeView: sharee resource being operated on.
-        @type shareeView: L{CommonHomeChildExternal}
-        @param objectResource: the resource being operated on, or C{None} for classmethod.
-        @type objectResource: L{CommonObjectResourceExternal}
-        @param transform: a function used to convert the JSON result into return values.
-        @type transform: C{callable}
-        @param args: list of optional arguments.
-        @type args: C{list}
-        @param kwargs: optional keyword arguments.
-        @type kwargs: C{dict}
-        """
-
-        action, recipient = yield self._send(actionName, shareeView, objectResource)
-        if args is not None:
-            action["arguments"] = args
-        if kwargs is not None:
-            action["keywords"] = kwargs
-        result = yield self.sendRequest(shareeView._txn, recipient, action)
-        if result["result"] == "ok":
-            returnValue(result["value"] if transform is None else transform(result["value"], shareeView, objectResource))
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def _simple_recv(self, txn, actionName, message, method, onHomeChild=True, transform=None):
-        """
-        A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
-        and include those only if present.
-
-        @param actionName: name of the action.
-        @type actionName: C{str}
-        @param message: message arguments
-        @type message: C{dict}
-        @param method: name of the method to execute on the shared resource to get the result.
-        @type method: C{str}
-        @param transform: method to call on returned JSON value to convert it to something useful.
-        @type transform: C{callable}
-        """
-
-        shareeView, objectResource = yield self._recv(txn, message, actionName)
-        try:
-            if onHomeChild:
-                # Operate on the L{CommonHomeChild}
-                value = yield getattr(shareeView, method)(*message.get("arguments", ()), **message.get("keywords", {}))
-            else:
-                # Operate on the L{CommonObjectResource}
-                if objectResource is not None:
-                    value = yield getattr(objectResource, method)(*message.get("arguments", ()), **message.get("keywords", {}))
-                else:
-                    # classmethod call
-                    value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *message.get("arguments", ()), **message.get("keywords", {}))
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        returnValue({
-            "result": "ok",
-            "value": transform(value, shareeView, objectResource) if transform is not None else value,
-        })
-
-
-    @inlineCallbacks
-    def send_freebusy(
-        self,
-        calresource,
-        timerange,
-        matchtotal,
-        excludeuid,
-        organizer,
-        organizerPrincipal,
-        same_calendar_user,
-        servertoserver,
-        event_details,
-    ):
-        action, recipient = yield self._send("freebusy", calresource)
-        action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
-        action["matchtotal"] = matchtotal
-        action["excludeuid"] = excludeuid
-        action["organizer"] = organizer
-        action["organizerPrincipal"] = organizerPrincipal
-        action["same_calendar_user"] = same_calendar_user
-        action["servertoserver"] = servertoserver
-        action["event_details"] = event_details
-        result = yield self.sendRequest(calresource._txn, recipient, action)
-        if result["result"] == "ok":
-            returnValue((result["fbresults"], result["matchtotal"],))
-        elif result["result"] == "exception":
-            raise namedClass(result["class"])(result["message"])
-
-
-    @inlineCallbacks
-    def recv_freebusy(self, txn, message):
-        """
-        Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}.
-
-        @param message: message arguments
-        @type message: C{dict}
-        """
-
-        shareeView, _ignore_objectResource = yield self._recv(txn, message, "freebusy")
-        try:
-            # Operate on the L{CommonHomeChild}
-            fbinfo = [[], [], []]
-            matchtotal = yield generateFreeBusyInfo(
-                shareeView,
-                fbinfo,
-                TimeRange(start=message["timerange"][0], end=message["timerange"][1]),
-                message["matchtotal"],
-                message["excludeuid"],
-                message["organizer"],
-                message["organizerPrincipal"],
-                message["same_calendar_user"],
-                message["servertoserver"],
-                message["event_details"],
-                logItems=None
-            )
-        except Exception as e:
-            returnValue({
-                "result": "exception",
-                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
-                "message": str(e),
-            })
-
-        for i in range(3):
-            for j in range(len(fbinfo[i])):
-                fbinfo[i][j] = fbinfo[i][j].getText()
-
-        returnValue({
-            "result": "ok",
-            "fbresults": fbinfo,
-            "matchtotal": matchtotal,
-        })
-
-
-    @staticmethod
-    def _to_tuple(value, shareeView, objectResource):
-        return tuple(value)
-
-
-    @staticmethod
-    def _to_string(value, shareeView, objectResource):
-        return str(value)
-
-
-    @staticmethod
-    def _to_externalize(value, shareeView, objectResource):
-        if isinstance(value, shareeView._objectResourceClass):
-            value = value.externalize()
-        elif value is not None:
-            value = [v.externalize() for v in value]
-        return value
-
-
-    @classmethod
-    def _make_simple_homechild_action(cls, action, method, transform_recv=None, transform_send=None):
-        setattr(
-            cls,
-            "send_{}".format(action),
-            lambda self, shareeView, *args, **kwargs:
-                self._simple_send(action, shareeView, transform=transform_send, args=args, kwargs=kwargs)
-        )
-        setattr(
-            cls,
-            "recv_{}".format(action),
-            lambda self, txn, message:
-                self._simple_recv(txn, action, message, method, transform=transform_recv)
-        )
-
-
-    @classmethod
-    def _make_simple_object_action(cls, action, method, transform_recv=None, transform_send=None):
-        setattr(
-            cls,
-            "send_{}".format(action),
-            lambda self, shareeView, objectResource, *args, **kwargs:
-                self._simple_send(action, shareeView, objectResource, transform=transform_send, args=args, kwargs=kwargs)
-        )
-        setattr(
-            cls,
-            "recv_{}".format(action),
-            lambda self, txn, message:
-                self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_recv)
-        )
-
-
-# Calls on L{CommonHomeChild} objects
-PoddingConduit._make_simple_homechild_action("countobjects", "countObjectResources")
-PoddingConduit._make_simple_homechild_action("listobjects", "listObjectResources")
-PoddingConduit._make_simple_homechild_action("resourceuidforname", "resourceUIDForName")
-PoddingConduit._make_simple_homechild_action("resourcenameforuid", "resourceNameForUID")
-PoddingConduit._make_simple_homechild_action("movehere", "moveObjectResourceHere")
-PoddingConduit._make_simple_homechild_action("moveaway", "moveObjectResourceAway")
-PoddingConduit._make_simple_homechild_action("synctoken", "syncToken")
-PoddingConduit._make_simple_homechild_action("resourcenamessincerevision", "resourceNamesSinceRevision", transform_send=PoddingConduit._to_tuple)
-PoddingConduit._make_simple_homechild_action("search", "search")
-
-# Calls on L{CommonObjectResource} objects
-PoddingConduit._make_simple_object_action("loadallobjects", "loadAllObjects", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("objectwith", "objectWith", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("create", "create", transform_recv=PoddingConduit._to_externalize)
-PoddingConduit._make_simple_object_action("setcomponent", "setComponent")
-PoddingConduit._make_simple_object_action("component", "component", transform_recv=PoddingConduit._to_string)
-PoddingConduit._make_simple_object_action("remove", "remove")

Added: CalendarServer/trunk/txdav/common/datastore/podding/directory.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/directory.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/directory.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -0,0 +1,215 @@
+##
+# Copyright (c) 2013-2014 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
+from twisted.python.reflect import namedClass
+from txdav.common.datastore.podding.base import FailedCrossPodRequestError
+from txdav.who.delegates import _delegatesOfUIDs, _delegatedToUIDs, setDelegates
+
+
+class DirectoryPoddingConduitMixin(object):
+    """
+    Defines the cross-pod API for common directory operations that will be mixed into the
+    L{PoddingConduit} class.
+    """
+
+    @inlineCallbacks
+    def send_set_delegates(self, txn, delegator, delegates, readWrite):
+        """
+        Set delegates for delegator on another pod.
+
+        @param txn: transaction to use
+        @type txn: L{CommonStoreTransaction}
+        @param delegator: delegator to set
+        @type delegator: L{DirectoryRecord}
+        @param delegates: delegates to set
+        @type delegates: L{list} of L{DirectoryRecord}
+        @param readWrite: if True, read and write access delegates are returned;
+            read-only access otherwise
+        """
+        if delegator.thisServer():
+            raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(delegator.uid))
+
+        request = {
+            "action": "set-delegates",
+            "uid": delegator.uid,
+            "delegates": [delegate.uid for delegate in delegates],
+            "read-write": readWrite,
+        }
+        response = yield self.sendRequestToServer(txn, delegator.server(), request)
+
+        if response["result"] == "ok":
+            returnValue(None)
+        elif response["result"] == "exception":
+            raise namedClass(response["class"])(response["result"])
+
+
+    @inlineCallbacks
+    def recv_set_delegates(self, txn, request):
+        """
+        Process a set delegates cross-pod request. Request arguments as per L{send_set_delegates}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        if request["action"] != "set-delegates":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_set_delegates".format(request["action"]))
+
+        try:
+            delegator = yield txn.directoryService().recordWithUID(request["uid"])
+            if delegator is None or not delegator.thisServer():
+                raise FailedCrossPodRequestError("Cross-pod delegate not on this server: {}".format(delegator.uid))
+
+            delegates = []
+            for uid in request["delegates"]:
+                delegate = yield txn.directoryService().recordWithUID(uid)
+                if delegate is None:
+                    raise FailedCrossPodRequestError("Cross-pod delegate missing on this server: {}".format(uid))
+                delegates.append(delegate)
+
+            yield setDelegates(txn, delegator, delegates, request["read-write"])
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "request": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+        })
+
+
+    @inlineCallbacks
+    def send_get_delegates(self, txn, delegator, readWrite, expanded=False):
+        """
+        Get delegates from another pod.
+
+        @param txn: transaction to use
+        @type txn: L{CommonStoreTransaction}
+        @param delegator: delegator to lookup
+        @type delegator: L{DirectoryRecord}
+        @param readWrite: if True, read and write access delegates are returned;
+            read-only access otherwise
+        """
+        if delegator.thisServer():
+            raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(delegator.uid))
+
+        request = {
+            "action": "get-delegates",
+            "uid": delegator.uid,
+            "read-write": readWrite,
+            "expanded": expanded,
+        }
+        response = yield self.sendRequestToServer(txn, delegator.server(), request)
+
+        if response["result"] == "ok":
+            returnValue(set(response["value"]))
+        elif response["result"] == "exception":
+            raise namedClass(response["class"])(response["result"])
+
+
+    @inlineCallbacks
+    def recv_get_delegates(self, txn, request):
+        """
+        Process an delegates cross-pod request. Request arguments as per L{send_get_delegates}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        if request["action"] != "get-delegates":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_get_delegates".format(request["action"]))
+
+        try:
+            delegator = yield txn.directoryService().recordWithUID(request["uid"])
+            if delegator is None or not delegator.thisServer():
+                raise FailedCrossPodRequestError("Cross-pod delegate not on this server: {}".format(delegator.uid))
+
+            delegates = yield _delegatesOfUIDs(txn, delegator, request["read-write"], request["expanded"])
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "request": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": list(delegates),
+        })
+
+
+    @inlineCallbacks
+    def send_get_delegators(self, txn, server, delegate, readWrite):
+        """
+        Get delegators from another pod.
+
+        @param txn: transaction to use
+        @type txn: L{CommonStoreTransaction}
+        @param server: server to query
+        @type server: L{Server}
+        @param delegate: delegate to lookup
+        @type delegate: L{DirectoryRecord}
+        @param readWrite: if True, read and write access delegates are returned;
+            read-only access otherwise
+        """
+        if not delegate.thisServer():
+            raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(delegate.uid))
+
+        request = {
+            "action": "get-delegators",
+            "uid": delegate.uid,
+            "read-write": readWrite,
+        }
+        response = yield self.sendRequestToServer(txn, server, request)
+
+        if response["result"] == "ok":
+            returnValue(set(response["value"]))
+        elif response["result"] == "exception":
+            raise namedClass(response["class"])(response["result"])
+
+
+    @inlineCallbacks
+    def recv_get_delegators(self, txn, request):
+        """
+        Process an delegators cross-pod request. Request arguments as per L{send_get_delegators}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        if request["action"] != "get-delegators":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_get_delegators".format(request["action"]))
+
+        try:
+            delegate = yield txn.directoryService().recordWithUID(request["uid"])
+            if delegate is None or delegate.thisServer():
+                raise FailedCrossPodRequestError("Cross-pod delegate missing or on this server: {}".format(delegate.uid))
+
+            delegateors = yield _delegatedToUIDs(txn, delegate, request["read-write"], onlyThisServer=True)
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "request": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": list(delegateors),
+        })

Added: CalendarServer/trunk/txdav/common/datastore/podding/sharing_base.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/sharing_base.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/sharing_base.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -0,0 +1,94 @@
+##
+# Copyright (c) 2013-2014 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
+
+from txdav.common.datastore.podding.base import FailedCrossPodRequestError
+
+
+class SharingCommonPoddingConduit(object):
+    """
+    Defines common cross-pod API for sharing that will be mixed into the L{PoddingConduit} class.
+    """
+
+    #
+    # Sharer data access related apis
+    #
+
+    @inlineCallbacks
+    def _getRequestForResource(self, action, parent, child=None):
+        """
+        Create a request for an operation on a L{CommonHomeChild}. This is used when building the JSON
+        request object prior to sending it.
+
+        @param shareeView: sharee resource being operated on.
+        @type shareeView: L{CommonHomeChildExternal}
+        """
+
+        homeType = parent.ownerHome()._homeType
+        ownerUID = parent.ownerHome().uid()
+        ownerID = parent.external_id()
+        shareeUID = parent.viewerHome().uid()
+
+        _ignore_sender, recipient = yield self.validRequest(shareeUID, ownerUID)
+
+        result = {
+            "action": action,
+            "type": homeType,
+            "owner": ownerUID,
+            "owner_id": ownerID,
+            "sharee": shareeUID,
+        }
+        if child is not None:
+            result["resource_id"] = child.id()
+        returnValue((result, recipient))
+
+
+    @inlineCallbacks
+    def _getResourcesForRequest(self, txn, request, expected_action):
+        """
+        Find the resources associated with the request. This is used when a JSON request has been received
+        and the underlying store objects the request refers to need to be found.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        if request["action"] != expected_action:
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(request["action"], expected_action))
+
+        # Get a share
+        ownerHome = yield txn.homeWithUID(request["type"], request["owner"])
+        if ownerHome is None or ownerHome.external():
+            FailedCrossPodRequestError("Invalid owner UID specified")
+
+        shareeHome = yield txn.homeWithUID(request["type"], request["sharee"])
+        if shareeHome is None or not shareeHome.external():
+            FailedCrossPodRequestError("Invalid sharee UID specified")
+
+        shareeView = yield shareeHome.childWithID(request["owner_id"])
+        if shareeView is None:
+            FailedCrossPodRequestError("Invalid shared resource specified")
+
+        resourceID = request.get("resource_id", None)
+        if resourceID is not None:
+            objectResource = yield shareeView.objectResourceWithID(resourceID)
+            if objectResource is None:
+                FailedCrossPodRequestError("Invalid owner shared object resource specified")
+        else:
+            objectResource = None
+
+        returnValue((shareeView, objectResource,))

Added: CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/sharing_invites.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -0,0 +1,246 @@
+##
+# Copyright (c) 2013-2014 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
+
+from txdav.common.icommondatastore import ExternalShareFailed
+from txdav.common.datastore.podding.base import FailedCrossPodRequestError
+from txdav.common.datastore.podding.sharing_base import SharingCommonPoddingConduit
+
+
+class SharingInvitesPoddingConduitMixin(SharingCommonPoddingConduit):
+    """
+    Defines the cross-pod API for sharing invites that will be mixed into the
+    L{PoddingConduit} class.
+    """
+
+    @inlineCallbacks
+    def send_shareinvite(self, txn, homeType, ownerUID, ownerID, ownerName, shareeUID, shareUID, bindMode, summary, copy_properties, supported_components):
+        """
+        Send a sharing invite cross-pod message.
+
+        @param homeType: Type of home being shared.
+        @type homeType: C{int}
+        @param ownerUID: UID of the sharer.
+        @type ownerUID: C{str}
+        @param ownerID: resource ID of the sharer calendar
+        @type ownerID: C{int}
+        @param ownerName: owner's name of the sharer calendar
+        @type ownerName: C{str}
+        @param shareeUID: UID of the sharee
+        @type shareeUID: C{str}
+        @param shareUID: Resource/invite ID for sharee
+        @type shareUID: C{str}
+        @param bindMode: bind mode for the share
+        @type bindMode: C{str}
+        @param summary: sharing message
+        @type summary: C{str}
+        @param copy_properties: C{str} name/value for properties to be copied
+        @type copy_properties: C{dict}
+        @param supported_components: supproted components, may be C{None}
+        @type supported_components: C{str}
+        """
+
+        _ignore_sender, recipient = yield self.validRequest(ownerUID, shareeUID)
+
+        request = {
+            "action": "shareinvite",
+            "type": homeType,
+            "owner": ownerUID,
+            "owner_id": ownerID,
+            "owner_name": ownerName,
+            "sharee": shareeUID,
+            "share_id": shareUID,
+            "mode": bindMode,
+            "summary": summary,
+            "properties": copy_properties,
+        }
+        if supported_components is not None:
+            request["supported-components"] = supported_components
+
+        result = yield self.sendRequest(txn, recipient, request)
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def recv_shareinvite(self, txn, request):
+        """
+        Process a sharing invite cross-pod request. Request arguments as per L{send_shareinvite}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        if request["action"] != "shareinvite":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareinvite".format(request["action"]))
+
+        # Sharee home on this pod must exist (create if needed)
+        shareeHome = yield txn.homeWithUID(request["type"], request["sharee"], create=True)
+        if shareeHome is None or shareeHome.external():
+            raise FailedCrossPodRequestError("Invalid sharee UID specified")
+
+        # Create a share
+        try:
+            yield shareeHome.processExternalInvite(
+                request["owner"],
+                request["owner_id"],
+                request["owner_name"],
+                request["share_id"],
+                request["mode"],
+                request["summary"],
+                request["properties"],
+                supported_components=request.get("supported-components")
+            )
+        except ExternalShareFailed as e:
+            raise FailedCrossPodRequestError(str(e))
+
+        returnValue({
+            "result": "ok",
+        })
+
+
+    @inlineCallbacks
+    def send_shareuninvite(self, txn, homeType, ownerUID, ownerID, shareeUID, shareUID):
+        """
+        Send a sharing uninvite cross-pod message.
+
+        @param homeType: Type of home being shared.
+        @type homeType: C{int}
+        @param ownerUID: UID of the sharer.
+        @type ownerUID: C{str}
+        @param ownerID: resource ID of the sharer calendar
+        @type ownerID: C{int}
+        @param shareeUID: UID of the sharee
+        @type shareeUID: C{str}
+        @param shareUID: Resource/invite ID for sharee
+        @type shareUID: C{str}
+        """
+
+        _ignore_sender, recipient = yield self.validRequest(ownerUID, shareeUID)
+
+        request = {
+            "action": "shareuninvite",
+            "type": homeType,
+            "owner": ownerUID,
+            "owner_id": ownerID,
+            "sharee": shareeUID,
+            "share_id": shareUID,
+        }
+
+        result = yield self.sendRequest(txn, recipient, request)
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def recv_shareuninvite(self, txn, request):
+        """
+        Process a sharing uninvite cross-pod request. Request arguments as per L{send_shareuninvite}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        if request["action"] != "shareuninvite":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareuninvite".format(request["action"]))
+
+        # Sharee home on this pod must already exist
+        shareeHome = yield txn.homeWithUID(request["type"], request["sharee"])
+        if shareeHome is None or shareeHome.external():
+            FailedCrossPodRequestError("Invalid sharee UID specified")
+
+        # Remove a share
+        try:
+            yield shareeHome.processExternalUninvite(
+                request["owner"],
+                request["owner_id"],
+                request["share_id"],
+            )
+        except ExternalShareFailed as e:
+            FailedCrossPodRequestError(str(e))
+
+        returnValue({
+            "result": "ok",
+        })
+
+
+    @inlineCallbacks
+    def send_sharereply(self, txn, homeType, ownerUID, shareeUID, shareUID, bindStatus, summary=None):
+        """
+        Send a sharing reply cross-pod message.
+
+        @param homeType: Type of home being shared.
+        @type homeType: C{int}
+        @param ownerUID: UID of the sharer.
+        @type ownerUID: C{str}
+        @param shareeUID: UID of the recipient
+        @type shareeUID: C{str}
+        @param shareUID: Resource/invite ID for recipient
+        @type shareUID: C{str}
+        @param bindStatus: bind mode for the share
+        @type bindStatus: C{str}
+        @param summary: sharing message
+        @type summary: C{str}
+        """
+
+        _ignore_sender, recipient = yield self.validRequest(shareeUID, ownerUID)
+
+        request = {
+            "action": "sharereply",
+            "type": homeType,
+            "owner": ownerUID,
+            "sharee": shareeUID,
+            "share_id": shareUID,
+            "status": bindStatus,
+        }
+        if summary is not None:
+            request["summary"] = summary
+
+        result = yield self.sendRequest(txn, recipient, request)
+        returnValue(result)
+
+
+    @inlineCallbacks
+    def recv_sharereply(self, txn, request):
+        """
+        Process a sharing reply cross-pod request. Request arguments as per L{send_sharereply}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        if request["action"] != "sharereply":
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_sharereply".format(request["action"]))
+
+        # Sharer home on this pod must already exist
+        ownerHome = yield txn.homeWithUID(request["type"], request["owner"])
+        if ownerHome is None or ownerHome.external():
+            FailedCrossPodRequestError("Invalid owner UID specified")
+
+        # Process a reply
+        try:
+            yield ownerHome.processExternalReply(
+                request["owner"],
+                request["sharee"],
+                request["share_id"],
+                request["status"],
+                summary=request.get("summary")
+            )
+        except ExternalShareFailed as e:
+            FailedCrossPodRequestError(str(e))
+
+        returnValue({
+            "result": "ok",
+        })

Added: CalendarServer/trunk/txdav/common/datastore/podding/sharing_store.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/sharing_store.py	                        (rev 0)
+++ CalendarServer/trunk/txdav/common/datastore/podding/sharing_store.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -0,0 +1,268 @@
+##
+# Copyright (c) 2013-2014 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
+from twisted.python.reflect import namedClass
+
+from txdav.common.datastore.podding.sharing_base import SharingCommonPoddingConduit
+from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+
+from twistedcaldav.caldavxml import TimeRange
+
+
+class SharingStorePoddingConduitMixin(SharingCommonPoddingConduit):
+    """
+    Defines the cross-pod API for access to shared resource data that will be mixed into the
+    L{PoddingConduit} class.
+    """
+
+    #
+    # Simple calls are ones where there is no argument and a single return value. We can simplify
+    # code generation for these by dynamically generating the appropriate class methods.
+    #
+
+    @inlineCallbacks
+    def _simple_send(self, actionName, shareeView, objectResource=None, transform=None, args=None, kwargs=None):
+        """
+        A simple send operation that returns a value.
+
+        @param actionName: name of the action.
+        @type actionName: C{str}
+        @param shareeView: sharee resource being operated on.
+        @type shareeView: L{CommonHomeChildExternal}
+        @param objectResource: the resource being operated on, or C{None} for classmethod.
+        @type objectResource: L{CommonObjectResourceExternal}
+        @param transform: a function used to convert the JSON response into return values.
+        @type transform: C{callable}
+        @param args: list of optional arguments.
+        @type args: C{list}
+        @param kwargs: optional keyword arguments.
+        @type kwargs: C{dict}
+        """
+
+        request, recipient = yield self._getRequestForResource(actionName, shareeView, objectResource)
+        if args is not None:
+            request["arguments"] = args
+        if kwargs is not None:
+            request["keywords"] = kwargs
+        response = yield self.sendRequest(shareeView._txn, recipient, request)
+        if response["result"] == "ok":
+            returnValue(response["value"] if transform is None else transform(response["value"], shareeView, objectResource))
+        elif response["result"] == "exception":
+            raise namedClass(response["class"])(response["result"])
+
+
+    @inlineCallbacks
+    def _simple_recv(self, txn, actionName, request, method, onHomeChild=True, transform=None):
+        """
+        A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
+        and include those only if present.
+
+        @param actionName: name of the action.
+        @type actionName: C{str}
+        @param request: request arguments
+        @type request: C{dict}
+        @param method: name of the method to execute on the shared resource to get the result.
+        @type method: C{str}
+        @param transform: method to call on returned JSON value to convert it to something useful.
+        @type transform: C{callable}
+        """
+
+        shareeView, objectResource = yield self._getResourcesForRequest(txn, request, actionName)
+        try:
+            if onHomeChild:
+                # Operate on the L{CommonHomeChild}
+                value = yield getattr(shareeView, method)(*request.get("arguments", ()), **request.get("keywords", {}))
+            else:
+                # Operate on the L{CommonObjectResource}
+                if objectResource is not None:
+                    value = yield getattr(objectResource, method)(*request.get("arguments", ()), **request.get("keywords", {}))
+                else:
+                    # classmethod call
+                    value = yield getattr(shareeView._objectResourceClass, method)(shareeView, *request.get("arguments", ()), **request.get("keywords", {}))
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "request": str(e),
+            })
+
+        returnValue({
+            "result": "ok",
+            "value": transform(value, shareeView, objectResource) if transform is not None else value,
+        })
+
+
+    @inlineCallbacks
+    def send_freebusy(
+        self,
+        calresource,
+        timerange,
+        matchtotal,
+        excludeuid,
+        organizer,
+        organizerPrincipal,
+        same_calendar_user,
+        servertoserver,
+        event_details,
+    ):
+        """
+        Request free busy information for a shared calendar collection hosted on a different pod. See
+        L{txdav.caldav.datastore.scheduling.freebusy} for the base free busy lookup behavior.
+        """
+        action, recipient = yield self._getRequestForResource("freebusy", calresource)
+        action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
+        action["matchtotal"] = matchtotal
+        action["excludeuid"] = excludeuid
+        action["organizer"] = organizer
+        action["organizerPrincipal"] = organizerPrincipal
+        action["same_calendar_user"] = same_calendar_user
+        action["servertoserver"] = servertoserver
+        action["event_details"] = event_details
+
+        response = yield self.sendRequest(calresource._txn, recipient, action)
+
+        if response["result"] == "ok":
+            returnValue((response["fbresults"], response["matchtotal"],))
+        elif response["result"] == "exception":
+            raise namedClass(response["class"])(response["result"])
+
+
+    @inlineCallbacks
+    def recv_freebusy(self, txn, request):
+        """
+        Process a freebusy cross-pod request. Message arguments as per L{send_freebusy}.
+
+        @param request: request arguments
+        @type request: C{dict}
+        """
+
+        shareeView, _ignore_objectResource = yield self._getResourcesForRequest(txn, request, "freebusy")
+        try:
+            # Operate on the L{CommonHomeChild}
+            fbinfo = [[], [], []]
+            matchtotal = yield generateFreeBusyInfo(
+                shareeView,
+                fbinfo,
+                TimeRange(start=request["timerange"][0], end=request["timerange"][1]),
+                request["matchtotal"],
+                request["excludeuid"],
+                request["organizer"],
+                request["organizerPrincipal"],
+                request["same_calendar_user"],
+                request["servertoserver"],
+                request["event_details"],
+                logItems=None
+            )
+        except Exception as e:
+            returnValue({
+                "result": "exception",
+                "class": ".".join((e.__class__.__module__, e.__class__.__name__,)),
+                "request": str(e),
+            })
+
+        # Convert L{DateTime} objects to text for JSON response
+        for i in range(3):
+            for j in range(len(fbinfo[i])):
+                fbinfo[i][j] = fbinfo[i][j].getText()
+
+        returnValue({
+            "result": "ok",
+            "fbresults": fbinfo,
+            "matchtotal": matchtotal,
+        })
+
+
+    #
+    # Methods used to transform arguments to or results from a JSON request or response.
+    #
+    @staticmethod
+    def _to_tuple(value, shareeView, objectResource):
+        return tuple(value)
+
+
+    @staticmethod
+    def _to_string(value, shareeView, objectResource):
+        return str(value)
+
+
+    @staticmethod
+    def _to_externalize(value, shareeView, objectResource):
+        """
+        Convert the value to the external (JSON-based) representation.
+        """
+        if isinstance(value, shareeView._objectResourceClass):
+            value = value.externalize()
+        elif value is not None:
+            value = [v.externalize() for v in value]
+        return value
+
+
+    #
+    # Factory methods for binding actions to the conduit class
+    #
+    @classmethod
+    def _make_simple_homechild_action(cls, action, method, transform_recv=None, transform_send=None):
+        setattr(
+            cls,
+            "send_{}".format(action),
+            lambda self, shareeView, *args, **kwargs:
+                self._simple_send(action, shareeView, transform=transform_send, args=args, kwargs=kwargs)
+        )
+        setattr(
+            cls,
+            "recv_{}".format(action),
+            lambda self, txn, message:
+                self._simple_recv(txn, action, message, method, transform=transform_recv)
+        )
+
+
+    @classmethod
+    def _make_simple_object_action(cls, action, method, transform_recv=None, transform_send=None):
+        setattr(
+            cls,
+            "send_{}".format(action),
+            lambda self, shareeView, objectResource, *args, **kwargs:
+                self._simple_send(action, shareeView, objectResource, transform=transform_send, args=args, kwargs=kwargs)
+        )
+        setattr(
+            cls,
+            "recv_{}".format(action),
+            lambda self, txn, message:
+                self._simple_recv(txn, action, message, method, onHomeChild=False, transform=transform_recv)
+        )
+
+# These are the actions on store objects we need to expose via the conduit api
+
+# Calls on L{CommonHomeChild} objects
+SharingStorePoddingConduitMixin._make_simple_homechild_action("countobjects", "countObjectResources")
+SharingStorePoddingConduitMixin._make_simple_homechild_action("listobjects", "listObjectResources")
+SharingStorePoddingConduitMixin._make_simple_homechild_action("resourceuidforname", "resourceUIDForName")
+SharingStorePoddingConduitMixin._make_simple_homechild_action("resourcenameforuid", "resourceNameForUID")
+SharingStorePoddingConduitMixin._make_simple_homechild_action("movehere", "moveObjectResourceHere")
+SharingStorePoddingConduitMixin._make_simple_homechild_action("moveaway", "moveObjectResourceAway")
+SharingStorePoddingConduitMixin._make_simple_homechild_action("synctoken", "syncToken")
+SharingStorePoddingConduitMixin._make_simple_homechild_action("resourcenamessincerevision", "resourceNamesSinceRevision", transform_send=SharingStorePoddingConduitMixin._to_tuple)
+SharingStorePoddingConduitMixin._make_simple_homechild_action("search", "search")
+
+# Calls on L{CommonObjectResource} objects
+SharingStorePoddingConduitMixin._make_simple_object_action("loadallobjects", "loadAllObjects", transform_recv=SharingStorePoddingConduitMixin._to_externalize)
+SharingStorePoddingConduitMixin._make_simple_object_action("loadallobjectswithnames", "loadAllObjectsWithNames", transform_recv=SharingStorePoddingConduitMixin._to_externalize)
+SharingStorePoddingConduitMixin._make_simple_object_action("objectwith", "objectWith", transform_recv=SharingStorePoddingConduitMixin._to_externalize)
+SharingStorePoddingConduitMixin._make_simple_object_action("create", "create", transform_recv=SharingStorePoddingConduitMixin._to_externalize)
+SharingStorePoddingConduitMixin._make_simple_object_action("setcomponent", "setComponent")
+SharingStorePoddingConduitMixin._make_simple_object_action("component", "component", transform_recv=SharingStorePoddingConduitMixin._to_string)
+SharingStorePoddingConduitMixin._make_simple_object_action("remove", "remove")

Modified: CalendarServer/trunk/txdav/who/cache.py
===================================================================
--- CalendarServer/trunk/txdav/who/cache.py	2014-10-16 16:09:16 UTC (rev 14083)
+++ CalendarServer/trunk/txdav/who/cache.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -82,6 +82,7 @@
     def __init__(self, directory, expireSeconds=30):
         BaseDirectoryService.__init__(self, directory.realmName)
         self._directory = directory
+        self.serversDB = directory.serversDB
         self._directoryTiming = hasattr(self._directory, "_addTiming")
         self._expireSeconds = expireSeconds
         self.resetCache()

Modified: CalendarServer/trunk/txdav/who/delegates.py
===================================================================
--- CalendarServer/trunk/txdav/who/delegates.py	2014-10-16 16:09:16 UTC (rev 14083)
+++ CalendarServer/trunk/txdav/who/delegates.py	2014-10-16 16:12:30 UTC (rev 14084)
@@ -20,8 +20,11 @@
 """
 
 from twisted.python.constants import Names, NamedConstant
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed, \
+    DeferredList
 
+from twistedcaldav.config import config
+
 from twext.python.log import Logger
 from twext.who.idirectory import (
     RecordType as BaseRecordType, FieldName, NotAllowedError
@@ -67,6 +70,7 @@
         this record.
         """
         parentUID, _ignore_proxyType = self.uid.split(u"#")
+        parentRecord = yield self.service._masterDirectory.recordWithUID(parentUID)
 
         @inlineCallbacks
         def _members(txn):
@@ -74,15 +78,11 @@
                 RecordType.readDelegateGroup, RecordType.writeDelegateGroup
             ):  # Members are delegates of this record
                 readWrite = (self.recordType is RecordType.writeDelegateGroup)
-                delegateUIDs = (
-                    yield txn.delegates(parentUID, readWrite, expanded=expanded)
-                )
+                delegateUIDs = yield _delegatesOfUIDs(txn, parentRecord, readWrite, expanded=expanded)
 
             else:  # Members have delegated to this record
                 readWrite = (self.recordType is RecordType.writeDelegatorGroup)
-                delegateUIDs = (
-                    yield txn.delegators(parentUID, readWrite)
-                )
+                delegateUIDs = yield _delegatedToUIDs(txn, parentRecord, readWrite)
             returnValue(delegateUIDs)
 
         delegateUIDs = yield self.service._store.inTransaction(
@@ -121,18 +121,13 @@
             m=[r.uid for r in memberRecords]
         )
 
-        @inlineCallbacks
+        delegator = (
+            yield self.service._masterDirectory.recordWithUID(parentUID)
+        )
+
         def _setMembers(txn):
-            yield txn.removeDelegates(parentUID, readWrite)
-            yield txn.removeDelegateGroups(parentUID, readWrite)
+            return setDelegates(txn, delegator, memberRecords, readWrite)
 
-            delegator = (
-                yield self.service._masterDirectory.recordWithUID(parentUID)
-            )
-
-            for delegate in memberRecords:
-                yield addDelegate(txn, delegator, delegate, readWrite)
-
         yield self.service._store.inTransaction(
             "DirectoryRecord.setMembers", _setMembers
         )
@@ -229,6 +224,32 @@
 
 
 @inlineCallbacks
+def setDelegates(txn, delegator, delegates, readWrite):
+    """
+    Sets the full set of delegates for a delegator.
+
+    We need to take multiple pods into account by re-directing this request
+    to the cross-pod conduit if the delegator is not local to this pod.
+
+    @param delegator: the delegator's directory record
+    @type delegator: L{IDirectoryRecord}
+    @param delegates: the delegates directory records
+    @type delegates: L{list}} of L{IDirectoryRecord}
+    @param readWrite: if True, read and write access is granted; read-only
+        access otherwise
+    """
+    if delegator.thisServer():
+        yield txn.removeDelegates(delegator.uid, readWrite)
+        yield txn.removeDelegateGroups(delegator.uid, readWrite)
+
+        for delegate in delegates:
+            yield addDelegate(txn, delegator, delegate, readWrite)
+    else:
+        yield _podSetDelegates(txn, delegator, delegates, readWrite)
+
+
+
+ at inlineCallbacks
 def addDelegate(txn, delegator, delegate, readWrite):
     """
     Adds "delegate" as a delegate of "delegator".  The type of access is
@@ -295,11 +316,10 @@
     @return: the set of directory records
     @rtype: a Deferred which fires a set of L{IDirectoryRecord}
     """
+    delegateUIDs = yield _delegatesOfUIDs(txn, delegator, readWrite, expanded)
+
     records = []
     directory = delegator.service
-    delegateUIDs = (
-        yield txn.delegates(delegator.uid, readWrite, expanded=expanded)
-    )
     for uid in delegateUIDs:
         if uid != delegator.uid:
             record = (yield directory.recordWithUID(uid))
@@ -322,12 +342,126 @@
     @return: the set of directory records
     @rtype: a Deferred which fires a set of L{IDirectoryRecord}
     """
+    delegatorUIDs = yield _delegatedToUIDs(txn, delegate, readWrite)
+
     records = []
     directory = delegate.service
-    delegatorUIDs = (yield txn.delegators(delegate.uid, readWrite))
     for uid in delegatorUIDs:
         if uid != delegate.uid:
             record = (yield directory.recordWithUID(uid))
             if record is not None:
                 records.append(record)
     returnValue(records)
+
+
+
+ at inlineCallbacks
+def _delegatesOfUIDs(txn, delegator, readWrite, expanded=False):
+    """
+    Return the UIDs of the delegates of "delegator".  The type of access
+    is specified by the "readWrite" parameter.
+
+    We need to take multiple pods into account by re-directing this request
+    to the cross-pod conduit if the delegator is not local to this pod.
+
+    @param delegator: the delegator's directory record
+    @type delegator: L{IDirectoryRecord}
+    @param readWrite: if True, read and write access delegates are returned;
+        read-only access otherwise
+    @return: the set of directory record uids
+    @rtype: a Deferred which fires a set of L{str}
+    """
+
+    if delegator.thisServer():
+        delegateUIDs = yield txn.delegates(delegator.uid, readWrite, expanded=expanded)
+    else:
+        delegateUIDs = yield _podDelegates(txn, delegator, readWrite, expanded=expanded)
+    returnValue(delegateUIDs)
+
+
+
+ at inlineCallbacks
+def _delegatedToUIDs(txn, delegate, readWrite, onlyThisServer=False):
+    """
+    Return the UIDs of those who have delegated to "delegate".  The type of
+    access is specified by the "readWrite" parameter.
+
+    We need to take multiple pods into account by re-directing this request
+    to the cross-pod conduit if the delegate is not local to this pod.
+
+    @param delegate: the delegate's directory record
+    @type delegate: L{IDirectoryRecord}
+    @param readWrite: if True, read and write access delegators are returned;
+        read-only access otherwise
+    @param onlyThisServer: used when doing the query as part of a cross-pod request since that
+        should only returns results for this server
+    @type onlyThisServer: L{bool}
+    @return: the set of directory record uids
+    @rtype: a Deferred which fires a set of L{str}
+    """
+
+
+    delegatorUIDs = (yield txn.delegators(delegate.uid, readWrite))
+    if not onlyThisServer and config.Servers.Enabled:
+        delegatorUIDs.update((yield _podDelegators(txn, delegate, readWrite)))
+    returnValue(delegatorUIDs)
+
+
+
+def _podSetDelegates(txn, delegator, delegates, readWrite):
+    """
+    Sets the full set of delegates for a delegator.
+
+    We need to take multiple pods into account by re-directing this request
+    to the cross-pod conduit if the delegator is not local to this pod.
+
+    @param delegator: the delegator's directory record
+    @type delegator: L{IDirectoryRecord}
+    @param delegates: the delegates directory records
+    @type delegates: L{list}} of L{IDirectoryRecord}
+    @param readWrite: if True, read and write access is granted; read-only
+        access otherwise
+    """
+    return txn.store().conduit.send_set_delegates(txn, delegator, delegates, readWrite)
+
+
+
+def _podDelegates(txn, delegator, readWrite, expanded=False):
+    """
+    Do a cross-pod request to get the delegates for this delegator.
+
+    @param delegator: the delegator's directory record
+    @type delegator: L{IDirectoryRecord}
+    @param readWrite: if True, read and write access delegates are returned;
+        read-only access otherwise
+    @return: the set of directory record uids
+    @rtype: a Deferred which fires a set of L{str}
+    """
+
+    return txn.store().conduit.send_get_delegates(txn, delegator, readWrite, expanded)
+
+
+
+ at inlineCallbacks
+def _podDelegators(txn, delegate, readWrite):
+    """
+    Do a cross-pod request to get the delegators for this delegate. We need to iterate over all
+    other pod servers to get results from each one.
+
+    @param delegate: the delegate's directory record
+    @type delegate: L{IDirectoryRecord}
+    @param readWrite: if True, read and write access delegates are returned;
+        read-only access otherwise
+    @return: the set of directory record uids
+    @rtype: a Deferred which fires a set of L{str}
+    """
+
+    results = yield DeferredList([
+        txn.store().conduit.send_get_delegators(txn, server, delegate, readWrite) for
+        server in txn.directoryService().serversDB.allServersExceptThis()
+    ], consumeErrors=True)
+    delegators = set()
+    for result in results:
+        if result and result[0]:
+            delegators.update(result[1])
+    returnValue(delegators)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20141016/9e9ef2b6/attachment-0001.html>


More information about the calendarserver-changes mailing list