[CalendarServer-changes] [10930] CalendarServer/branches/users/sagen/testing

source_changes at macosforge.org source_changes at macosforge.org
Fri Mar 15 10:01:43 PDT 2013


Revision: 10930
          http://trac.calendarserver.org//changeset/10930
Author:   sagen at apple.com
Date:     2013-03-15 10:01:43 -0700 (Fri, 15 Mar 2013)
Log Message:
-----------
Pull up changes from trunk

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/testing/calendarserver/push/amppush.py
    CalendarServer/branches/users/sagen/testing/calendarserver/push/applepush.py
    CalendarServer/branches/users/sagen/testing/calendarserver/push/notifier.py
    CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_amppush.py
    CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_applepush.py
    CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_notifier.py
    CalendarServer/branches/users/sagen/testing/calendarserver/tools/config.py
    CalendarServer/branches/users/sagen/testing/calendarserver/tools/gateway.py
    CalendarServer/branches/users/sagen/testing/calendarserver/tools/purge.py
    CalendarServer/branches/users/sagen/testing/calendarserver/tools/push.py
    CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/gateway/caldavd.plist
    CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/test_gateway.py
    CalendarServer/branches/users/sagen/testing/calendarserver/tools/upgrade.py
    CalendarServer/branches/users/sagen/testing/conf/caldavd-apple.plist
    CalendarServer/branches/users/sagen/testing/run
    CalendarServer/branches/users/sagen/testing/setup.py
    CalendarServer/branches/users/sagen/testing/support/Makefile.Apple
    CalendarServer/branches/users/sagen/testing/twext/enterprise/queue.py
    CalendarServer/branches/users/sagen/testing/twext/enterprise/test/test_queue.py
    CalendarServer/branches/users/sagen/testing/twistedcaldav/config.py
    CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/branches/users/sagen/testing/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/sagen/testing/twistedcaldav/test/test_config.py
    CalendarServer/branches/users/sagen/testing/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql.py
    CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql
    CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql

Added Paths:
-----------
    CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/test_config.py
    CalendarServer/branches/users/sagen/testing/doc/calendarserver_config.8

Removed Paths:
-------------
    CalendarServer/branches/users/sagen/testing/contrib/migration/__init__.py
    CalendarServer/branches/users/sagen/testing/contrib/migration/calendarcommonextra.py
    CalendarServer/branches/users/sagen/testing/contrib/migration/calendardemotion.py
    CalendarServer/branches/users/sagen/testing/contrib/migration/calendarmigrator.py
    CalendarServer/branches/users/sagen/testing/contrib/migration/calendarpromotion.py
    CalendarServer/branches/users/sagen/testing/contrib/migration/test/

Property Changed:
----------------
    CalendarServer/branches/users/sagen/testing/


Property changes on: CalendarServer/branches/users/sagen/testing
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:10857-10898
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
/CalendarServer/trunk:10857-10929

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/push/amppush.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/push/amppush.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/push/amppush.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -71,7 +71,7 @@
         controlSocket.addFactory(PUSH_ROUTE, AMPPushForwardingFactory(self))
 
     @inlineCallbacks
-    def enqueue(self, id, dataChangedTimestamp=None):
+    def enqueue(self, transaction, id, dataChangedTimestamp=None):
         if dataChangedTimestamp is None:
             dataChangedTimestamp = int(time.time())
         for protocol in self.protocols:
@@ -92,7 +92,7 @@
     def enqueueFromWorker(self, id, dataChangedTimestamp=None):
         if dataChangedTimestamp is None:
             dataChangedTimestamp = int(time.time())
-        self.master.enqueue(id, dataChangedTimestamp=dataChangedTimestamp)
+        self.master.enqueue(None, id, dataChangedTimestamp=dataChangedTimestamp)
         return {"status" : "OK"}
  
 
@@ -145,7 +145,7 @@
         self.log_debug("Removed subscriber")
         self.subscribers.remove(p)
 
-    def enqueue(self, pushKey, dataChangedTimestamp=None):
+    def enqueue(self, transaction, pushKey, dataChangedTimestamp=None):
         """
         Sends an AMP push notification to any clients subscribing to this pushKey.
 

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/push/applepush.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/push/applepush.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/push/applepush.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -176,7 +176,7 @@
 
 
     @inlineCallbacks
-    def enqueue(self, pushKey, dataChangedTimestamp=None):
+    def enqueue(self, transaction, pushKey, dataChangedTimestamp=None):
         """
         Sends an Apple Push Notification to any device token subscribed to
         this pushKey.
@@ -207,9 +207,7 @@
         if provider is not None:
 
             # Look up subscriptions for this key
-            txn = self.store.newTransaction()
-            subscriptions = (yield txn.apnSubscriptionsByKey(pushKey))
-            yield txn.commit()
+            subscriptions = (yield transaction.apnSubscriptionsByKey(pushKey))
 
             numSubscriptions = len(subscriptions)
             if numSubscriptions > 0:

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/push/notifier.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/push/notifier.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/push/notifier.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -44,7 +44,7 @@
 
         pushDistributor = self.transaction._pushDistributor
         if pushDistributor is not None:
-            yield pushDistributor.enqueue(self.pushID)
+            yield pushDistributor.enqueue(self.transaction, self.pushID)
 
 
 
@@ -204,12 +204,15 @@
         self.observers = observers 
 
     @inlineCallbacks
-    def enqueue(self, pushKey):
+    def enqueue(self, transaction, pushKey):
         """
         Pass along enqueued pushKey to any observers
 
+        @param transaction: a transaction to use, if needed
+        @type transaction: L{CommonStoreTransaction}
+
         @param pushKey: the push key to distribute to the observers
         @type pushKey: C{str}
         """
         for observer in self.observers:
-            yield observer.enqueue(pushKey)
+            yield observer.enqueue(transaction, pushKey)

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_amppush.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_amppush.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_amppush.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -57,7 +57,7 @@
         self.assertTrue(client3.subscribedToID("/CalDAV/localhost/user03/"))
 
         dataChangedTimestamp = 1354815999
-        service.enqueue("/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
+        service.enqueue(None, "/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
         self.assertEquals(len(client1.history), 0)
         self.assertEquals(len(client2.history), 0)
         self.assertEquals(len(client3.history), 0)
@@ -74,7 +74,7 @@
         client1.reset()
         client2.reset()
         client2.unsubscribe("token2", "/CalDAV/localhost/user01/")
-        service.enqueue("/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
+        service.enqueue(None, "/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
         self.assertEquals(len(client1.history), 0)
         clock.advance(1)
         self.assertEquals(client1.history, [(NotificationForID, {'id': '/CalDAV/localhost/user01/', 'dataChangedTimestamp' : 1354815999})])
@@ -87,7 +87,7 @@
         client1.reset()
         client2.reset()
         client2.subscribe("token2", "/CalDAV/localhost/user01/")
-        service.enqueue("/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
+        service.enqueue(None, "/CalDAV/localhost/user01/", dataChangedTimestamp=dataChangedTimestamp)
         self.assertEquals(client1.history, [(NotificationForID, {'id': '/CalDAV/localhost/user01/', 'dataChangedTimestamp' : 1354815999})])
         self.assertEquals(client2.history, [(NotificationForID, {'id': '/CalDAV/localhost/user01/', 'dataChangedTimestamp' : 1354815999})])
 

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_applepush.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_applepush.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_applepush.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -125,8 +125,10 @@
 
         # Notification arrives from calendar server
         dataChangedTimestamp = 1354815999
-        yield service.enqueue("/CalDAV/calendars.example.com/user01/calendar/",
+        txn = self.store.newTransaction()
+        yield service.enqueue(txn, "/CalDAV/calendars.example.com/user01/calendar/",
             dataChangedTimestamp=dataChangedTimestamp)
+        yield txn.commit()
 
         # The notifications should be in the queue
         self.assertTrue(((token, key1), dataChangedTimestamp) in service.providers["CalDAV"].queue)
@@ -165,7 +167,9 @@
         # Reset sent data
         providerConnector.transport.data = None
         # Send notification while service is connected
-        yield service.enqueue("/CalDAV/calendars.example.com/user01/calendar/")
+        txn = self.store.newTransaction()
+        yield service.enqueue(txn, "/CalDAV/calendars.example.com/user01/calendar/")
+        yield txn.commit()
         clock.advance(1) # so that first push is sent
         self.assertEquals(len(providerConnector.transport.data), 183)
         # Reset sent data

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_notifier.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_notifier.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/push/test/test_notifier.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -30,7 +30,7 @@
     def reset(self):
         self.history = []
 
-    def enqueue(self, id):
+    def enqueue(self, transaction, id):
         self.history.append(id)
         return(succeed(None))
 
@@ -40,7 +40,7 @@
     def test_enqueue(self):
         stub = StubService()
         dist = PushDistributor([stub])
-        yield dist.enqueue("testing")
+        yield dist.enqueue(None, "testing")
         self.assertEquals(stub.history, ["testing"])
 
     def test_getPubSubAPSConfiguration(self):
@@ -82,7 +82,7 @@
     def reset(self):
         self.history = []
 
-    def enqueue(self, pushID):
+    def enqueue(self, transaction, pushID):
         self.history.append(pushID)
 
 class PushNotificationWorkTests(TestCase):
@@ -117,6 +117,13 @@
         wp = (yield txn.enqueue(PushNotificationWork,
             pushID="/CalDAV/localhost/bar/",
         ))
+        # Enqueue a different pushID to ensure those are not grouped with
+        # the others:
+        wp = (yield txn.enqueue(PushNotificationWork,
+            pushID="/CalDAV/localhost/baz/",
+        ))
+
         yield txn.commit()
         yield wp.whenExecuted()
-        self.assertEquals(pushDistributor.history, ["/CalDAV/localhost/bar/"])
+        self.assertEquals(pushDistributor.history,
+            ["/CalDAV/localhost/bar/", "/CalDAV/localhost/baz/"])

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/tools/config.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/tools/config.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/tools/config.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -18,18 +18,56 @@
 from __future__ import print_function
 
 """
-This tool reads the Calendar Server configuration file and emits the
-requested value.
+This tool gets and sets Calendar Server configuration keys
 """
 
-import os, sys
 from getopt import getopt, GetoptError
+import os
+import plistlib
+import signal
+import sys
+import xml
 
-from twistedcaldav.config import ConfigurationError
+from twext.python.plistlib import readPlistFromString, writePlistToString
+from twistedcaldav.config import config, ConfigDict, ConfigurationError, mergeData
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+WRITABLE_CONFIG_KEYS = [
+    "EnableSSL",
+    "RedirectHTTPToHTTPS",
+    "EnableCalDAV",
+    "EnableCardDAV",
+    "DataRoot",
+    "SSLCertificate",
+    "SSLPrivateKey",
+    "SSLAuthorityChain",
+    "EnableSearchAddressBook",
+    "Authentication.Basic.Enabled",
+    "Authentication.Basic.AllowedOverWireUnencrypted",
+    "Authentication.Digest.Enabled",
+    "Authentication.Digest.AllowedOverWireUnencrypted",
+    "Authentication.Kerberos.Enabled",
+    "Authentication.Kerberos.AllowedOverWireUnencrypted",
+    "Authentication.Wiki.Enabled",
+    "Scheduling.iMIP.Enabled",
+    "Scheduling.iMIP.Receiving.Username",
+    "Scheduling.iMIP.Receiving.Server",
+    "Scheduling.iMIP.Receiving.Port",
+    "Scheduling.iMIP.Receiving.Type",
+    "Scheduling.iMIP.Receiving.UseSSL",
+    "Scheduling.iMIP.Sending.Username",
+    "Scheduling.iMIP.Sending.Server",
+    "Scheduling.iMIP.Sending.Port",
+    "Scheduling.iMIP.Sending.UseSSL",
+    "Scheduling.iMIP.Sending.Address",
+    "Notifications.Services.APNS.Enabled",
+    "Notifications.Services.APNS.CalDAV.CertificatePath",
+    "Notifications.Services.APNS.CalDAV.AuthorityChainPath",
+    "Notifications.Services.APNS.CalDAV.PrivateKeyPath",
+    "Notifications.Services.APNS.CardDAV.CertificatePath",
+    "Notifications.Services.APNS.CardDAV.AuthorityChainPath",
+    "Notifications.Services.APNS.CardDAV.PrivateKeyPath",
+]
 
-from calendarserver.tools.util import loadConfig
-
 def usage(e=None):
     if e:
         print(e)
@@ -42,6 +80,7 @@
     print("options:")
     print("  -h --help: print this help and exit")
     print("  -f --config: Specify caldavd.plist configuration path")
+    print("  -w --writeconfig: Specify caldavd.plist configuration path for writing")
 
     if e:
         sys.exit(64)
@@ -51,15 +90,17 @@
 def main():
     try:
         (optargs, args) = getopt(
-            sys.argv[1:], "hf:", [
+            sys.argv[1:], "hf:w:", [
                 "help",
                 "config=",
+                "writeconfig=",
             ],
         )
     except GetoptError, e:
         usage(e)
 
     configFileName = DEFAULT_CONFIG_FILE
+    writeConfigFileName = ""
 
     for opt, arg in optargs:
         if opt in ("-h", "--help"):
@@ -68,18 +109,332 @@
         elif opt in ("-f", "--config"):
             configFileName = arg
 
+        elif opt in ("-w", "--writeconfig"):
+            writeConfigFileName = arg
+
     try:
-        config = loadConfig(configFileName)
+        config.load(configFileName)
     except ConfigurationError, e:
         sys.stdout.write("%s\n" % (e,))
         sys.exit(1)
 
-    for configKey in args:
-        c = config
-        for subKey in configKey.split("."):
-            c = c.get(subKey, None)
-            if c is None:
-                sys.stderr.write("No such config key: %s\n" % configKey)
-                break
+    if not writeConfigFileName:
+        # If --writeconfig was not passed, use WritableConfigFile from
+        # main plist.  If that's an empty string, writes will happen to
+        # the main file.
+        writeConfigFileName = config.WritableConfigFile
+
+    if not writeConfigFileName:
+        writeConfigFileName = configFileName
+
+    writable = WritableConfig(config, writeConfigFileName)
+    writable.read()
+
+    if args:
+        for configKey in args:
+
+            if "=" in configKey:
+                # This is an assignment
+                configKey, stringValue = configKey.split("=")
+                value = writable.convertToValue(stringValue)
+                writable.set({configKey:value})
+            else:
+                # This is a read
+                c = config
+                for subKey in configKey.split("."):
+                    c = c.get(subKey, None)
+                    if c is None:
+                        sys.stderr.write("No such config key: %s\n" % configKey)
+                        break
+                sys.stdout.write("%s=%s\n" % (configKey, c))
+
+        writable.save(restart=True)
+
+    else:
+        # Read plist commands from stdin
+        rawInput = sys.stdin.read()
+        try:
+            plist = readPlistFromString(rawInput)
+        except xml.parsers.expat.ExpatError, e:
+            respondWithError(str(e))
+            return
+
+        # If the plist is an array, each element of the array is a separate
+        # command dictionary.
+        if isinstance(plist, list):
+            commands = plist
         else:
-            sys.stdout.write("%s\n" % c)
+            commands = [plist]
+
+        runner = Runner(commands)
+        runner.run()
+
+
+
+class Runner(object):
+
+    """
+    A class which carries out commands, which are plist strings containing
+    dictionaries with a "command" key, plus command-specific data.
+    """
+
+    def __init__(self, commands):
+        """
+        @param commands: the commands to run
+        @type commands: list of plist strings
+        """
+        self.commands = commands
+
+    def validate(self):
+        """
+        Validate all the commands by making sure this class implements
+        all the command keys.
+        @return: True if all commands are valid, False otherwise
+        """
+        # Make sure commands are valid
+        for command in self.commands:
+            if 'command' not in command:
+                respondWithError("'command' missing from plist")
+                return False
+            commandName = command['command']
+            methodName = "command_%s" % (commandName,)
+            if not hasattr(self, methodName):
+                respondWithError("Unknown command '%s'" % (commandName,))
+                return False
+        return True
+
+    def run(self):
+        """
+        Find the appropriate method for each command and call them.
+        """
+        try:
+            for command in self.commands:
+                commandName = command['command']
+                methodName = "command_%s" % (commandName,)
+                if hasattr(self, methodName):
+                    getattr(self, methodName)(command)
+                else:
+                    respondWithError("Unknown command '%s'" % (commandName,))
+
+        except Exception, e:
+            respondWithError("Command failed: '%s'" % (str(e),))
+            raise
+
+    def command_readConfig(self, command):
+        """
+        Return current configuration
+
+        @param command: the dictionary parsed from the plist read from stdin
+        @type command: C{dict}
+        """
+        result = {}
+        for keyPath in WRITABLE_CONFIG_KEYS:
+            value = getKeyPath(config, keyPath)
+            if value is not None:
+                setKeyPath(result, keyPath, value)
+        respond(command, result)
+
+    def command_writeConfig(self, command):
+        """
+        Write config to secondary, writable plist
+
+        @param command: the dictionary parsed from the plist read from stdin
+        @type command: C{dict}
+        """
+        writable = WritableConfig(config, config.WritableConfigFile)
+        writable.read()
+        valuesToWrite = command.get("Values", {})
+        for keyPath, value in flattenDictionary(valuesToWrite):
+            if keyPath in WRITABLE_CONFIG_KEYS:
+                writable.set(setKeyPath(ConfigDict(), keyPath, value))
+        try:
+            writable.save(restart=False)
+        except Exception, e:
+            respond(command, {"error": str(e)})
+        else:
+            config.reload()
+            self.command_readConfig(command)
+
+
+def setKeyPath(parent, keyPath, value):
+    """
+    Allows the setting of arbitrary nested dictionary keys via a single
+    dot-separated string.  For example, setKeyPath(parent, "foo.bar.baz",
+    "xyzzy") would create any intermediate missing directories (or whatever
+    class parent is, such as ConfigDict) so that the following structure
+    results:  parent = { "foo" : { "bar" : { "baz" : "xyzzy } } }
+
+    @param parent: the object to modify
+    @type parent: any dict-like object
+    @param keyPath: a dot-delimited string specifying the path of keys to
+        traverse
+    @type keyPath: C{str}
+    @param value: the value to set
+    @type value: c{object}
+    @return: parent
+    """
+    original = parent
+    parts = keyPath.split(".")
+    for part in parts[:-1]:
+        child = parent.get(part, None)
+        if child is None:
+            parent[part] = child = parent.__class__()
+        parent = child
+    parent[parts[-1]] = value
+    return original
+
+def getKeyPath(parent, keyPath):
+    """
+    Allows the getting of arbitrary nested dictionary keys via a single
+    dot-separated string.  For example, getKeyPath(parent, "foo.bar.baz")
+    would fetch parent["foo"]["bar"]["baz"].  If any of the keys don't
+    exist, None is returned instead.
+
+    @param parent: the object to traverse
+    @type parent: any dict-like object
+    @param keyPath: a dot-delimited string specifying the path of keys to
+        traverse
+    @type keyPath: C{str}
+    @return: the value at keyPath
+    """
+    parts = keyPath.split(".")
+    for part in parts[:-1]:
+        child = parent.get(part, None)
+        if child is None:
+            return None
+        parent = child
+    return parent.get(parts[-1], None)
+
+def flattenDictionary(dictionary, current=""):
+    """
+    Returns a generator of (keyPath, value) tuples for the given dictionary,
+    where each keyPath is a dot-separated string representing the complete
+    path to a nested key.
+
+    @param dictionary: the dict object to traverse
+    @type dictionary: C{dict}
+    @param current: do not use; used internally for recursion
+    @type current: C{str}
+    @return: generator of (keyPath, value) tuples
+    """
+    for key, value in dictionary.iteritems():
+        if isinstance(value, dict):
+            for result in flattenDictionary(value, current + key + "."):
+                yield result
+        else:
+            yield (current + key, value)
+
+
+def restartService(pidFilename):
+    """
+    Given the path to a PID file, sends a HUP signal to the contained pid
+    in order to cause calendar server to restart.
+
+    @param pidFilename: an absolute path to a PID file
+    @type pidFilename: C{str}
+    """
+    if os.path.exists(pidFilename):
+        pidFile = open(pidFilename, "r")
+        pid = pidFile.read().strip()
+        pidFile.close()
+        try:
+            pid = int(pid)
+        except ValueError:
+            return
+        try:
+            os.kill(pid, signal.SIGHUP)
+        except OSError:
+            pass
+
+
+class WritableConfig(object):
+    """
+    A wrapper around a Config object which allows writing of values.  The idea
+    is a deployment could have a master plist which doesn't change, and have
+    it include a plist file which does.  This class facilitates writing to that
+    included plist.
+    """
+
+    def __init__(self, wrappedConfig, fileName):
+        """
+        @param wrappedConfig: the Config object to read from
+        @type wrappedConfig: C{Config}
+        @param fileName: the full path to the modifiable plist
+        @type fileName: C{str}
+        """
+        self.config = wrappedConfig
+        self.fileName = fileName
+        self.changes = None
+        self.currentConfigSubset = ConfigDict()
+        self.dirty = False
+
+    def set(self, data):
+        """
+        Merges data into a ConfigDict of changes intended to be saved to disk
+        when save( ) is called.
+
+        @param data: a dict containing new values
+        @type data: C{dict}
+        """
+        if not isinstance(data, ConfigDict):
+            data = ConfigDict(mapping=data)
+        mergeData(self.currentConfigSubset, data)
+        self.dirty = True
+
+    def read(self):
+        """
+        Reads in the data contained in the writable plist file.
+
+        @return: C{ConfigDict}
+        """
+        if os.path.exists(self.fileName):
+            self.currentConfigSubset = ConfigDict(mapping=plistlib.readPlist(self.fileName))
+        else:
+            self.currentConfigSubset = ConfigDict()
+
+    def toString(self):
+        return plistlib.writePlistToString(self.currentConfigSubset)
+
+    def save(self, restart=False):
+        """
+        Writes any outstanding changes to the writable plist file.  Optionally
+        restart calendar server.
+
+        @param restart: whether to restart the calendar server.
+        @type restart: C{bool}
+        """
+        if self.dirty:
+            plistlib.writePlist(self.currentConfigSubset, self.fileName)
+            self.dirty = False
+            if restart:
+                restartService(self.config.PIDFile)
+
+    @classmethod
+    def convertToValue(cls, string):
+        """
+        Inspect string and convert the value into an appropriate Python data type
+        TODO: change this to look at actual types definied within stdconfig
+        """
+        if "." in string:
+            try:
+                value = float(string)
+            except ValueError:
+                value = string
+        else:
+            try:
+                value = int(string)
+            except ValueError:
+                if string == "True":
+                    value = True
+                elif string == "False":
+                    value = False
+                else:
+                    value = string
+        return value
+
+
+def respond(command, result):
+    sys.stdout.write(writePlistToString({'command' : command['command'], 'result' : result}))
+
+def respondWithError(msg, status=1):
+    sys.stdout.write(writePlistToString({'error' : msg, }))

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/tools/gateway.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/tools/gateway.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/tools/gateway.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -465,7 +465,6 @@
         respond(command, {'EventsRemoved' : eventCount, "RetainDays" : retainDays})
 
 
-
 @inlineCallbacks
 def respondWithProxies(directory, command, principal, proxyType):
     proxies = []
@@ -508,21 +507,13 @@
     respond(command, result)
 
 
-
 def respond(command, result):
     sys.stdout.write(writePlistToString({'command' : command['command'], 'result' : result}))
 
 
-
 def respondWithError(msg, status=1):
     sys.stdout.write(writePlistToString({'error' : msg, }))
-    """
-    try:
-        reactor.stop()
-    except RuntimeError:
-        pass
-    sys.exit(status)
-    """
 
+
 if __name__ == "__main__":
     main()

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/tools/purge.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/tools/purge.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -792,10 +792,14 @@
             # FIXME: probably want a more elegant way to accomplish this,
             # since it requires the aggregate directory to examine these first:
             record = DirectoryRecord(self.directory, "users", uid, shortNames=(uid,), enabledForCalendaring=True)
-            record.enabled = True
             self.directory._tmpRecords["shortNames"][uid] = record
             self.directory._tmpRecords["uids"][uid] = record
 
+        # Override augments settings for this record
+        record.enabled = True
+        record.enabledForCalendaring = True
+        record.enabledForAddressBooks = True
+
         cua = "urn:uuid:%s" % (uid,)
 
         principalCollection = self.directory.principalCollection

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/tools/push.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/tools/push.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/tools/push.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -19,39 +19,18 @@
 from calendarserver.tap.util import getRootResource
 from calendarserver.tools.cmdline import utilityMain
 from errno import ENOENT, EACCES
-from getopt import getopt, GetoptError
+from argparse import ArgumentParser
 from twext.python.log import Logger
 from twisted.application.service import Service
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks
 from twistedcaldav.config import config, ConfigurationError
-import os
 import sys
 import time
 
 log = Logger()
 
-def usage(e=None):
 
-    name = os.path.basename(sys.argv[0])
-    print("usage: %s [options] [user ...]" % (name,))
-    print("")
-    print("  Display Apple Push Notification subscriptions")
-    print("")
-    print("options:")
-    print("  -h --help: print this help and exit")
-    print("  -f --config <path>: Specify caldavd.plist configuration path")
-    print("  -D --debug: debug logging")
-    print("")
-
-    if e:
-        sys.stderr.write("%s\n" % (e,))
-        sys.exit(64)
-    else:
-        sys.exit(0)
-
-
-
 class WorkerService(Service):
 
     def __init__(self, store):
@@ -109,45 +88,18 @@
 
 def main():
 
-    try:
-        (optargs, args) = getopt(
-            sys.argv[1:], "Df:h", [
-                "config=",
-                "help",
-                "debug",
-            ],
-        )
-    except GetoptError, e:
-        usage(e)
+    parser = ArgumentParser(description='Display Apple Push Notification subscriptions')
+    parser.add_argument('-f', '--config', dest='configFileName', metavar='CONFIGFILE', help='caldavd.plist configuration file path')
+    parser.add_argument('-d', '--debug', action='store_true', help='show debug logging')
+    parser.add_argument('user', help='one or more users to display', nargs='+') # Required
+    args = parser.parse_args()
 
-    #
-    # Get configuration
-    #
-    configFileName = None
-    debug = False
+    DisplayAPNSubscriptions.users = args.user
 
-    for opt, arg in optargs:
-        if opt in ("-h", "--help"):
-            usage()
-
-        elif opt in ("-f", "--config"):
-            configFileName = arg
-
-        if opt in ("-d", "--debug"):
-            debug = True
-
-        else:
-            raise NotImplementedError(opt)
-
-    if not args:
-        usage("Not enough arguments")
-
-    DisplayAPNSubscriptions.users = args
-
     utilityMain(
-        configFileName,
+        args.configFileName,
         DisplayAPNSubscriptions,
-        verbose=debug,
+        verbose=args.debug,
     )
 
 

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/gateway/caldavd.plist
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/gateway/caldavd.plist	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/gateway/caldavd.plist	2013-03-15 17:01:43 UTC (rev 10930)
@@ -34,6 +34,14 @@
     <key>ServerHostName</key>
     <string></string> <!-- The hostname clients use when connecting -->
 
+    <!-- Enable Calendars -->
+    <key>EnableCalDAV</key>
+    <true/>
+
+    <!-- Enable AddressBooks -->
+    <key>EnableCardDAV</key>
+    <true/>
+
     <!-- HTTP port [0 = disable HTTP] -->
     <key>HTTPPort</key>
     <integer>8008</integer>
@@ -87,7 +95,7 @@
 
     <!-- Configuration root -->
     <key>ConfigRoot</key>
-    <string>/etc/caldavd</string>
+    <string>config</string>
 
     <!-- Log root -->
     <key>LogRoot</key>
@@ -496,65 +504,31 @@
 
       <key>Services</key>
       <dict>
-        <key>SimpleLineNotifier</key>
-        <dict>
-          <!-- Simple line notification service (for testing) -->
-          <key>Service</key>
-          <string>twistedcaldav.notify.SimpleLineNotifierService</string>
-          <key>Enabled</key>
-          <false/>
-          <key>Port</key>
-          <integer>62308</integer>
-        </dict>
 
-        <key>XMPPNotifier</key>
+        <key>APNS</key>
         <dict>
-          <!-- XMPP notification service -->
-          <key>Service</key>
-          <string>twistedcaldav.notify.XMPPNotifierService</string>
           <key>Enabled</key>
           <false/>
-
-          <!-- XMPP host and port to contact -->
-          <key>Host</key>
-          <string>xmpp.host.name</string>
-          <key>Port</key>
-          <integer>5222</integer>
-
-          <!-- Jabber ID and password for the server -->
-          <key>JID</key>
-          <string>jid at xmpp.host.name/resource</string>
-          <key>Password</key>
-          <string>password_goes_here</string>
-
-          <!-- PubSub service address -->
-          <key>ServiceAddress</key>
-          <string>pubsub.xmpp.host.name</string>
-
-          <key>NodeConfiguration</key>
+          <key>EnableStaggering</key>
+          <true/>
+          <key>StaggerSeconds</key>
+          <integer>5</integer>
+          <key>CalDAV</key>
           <dict>
-            <key>pubsub#deliver_payloads</key>
-            <string>1</string>
-            <key>pubsub#persist_items</key>
-            <string>1</string>
+            <key>CertificatePath</key>
+            <string>/example/calendar.cer</string>
+            <key>PrivateKeyPath</key>
+            <string>/example/calendar.pem</string>
           </dict>
-
-          <!-- Sends a presence notification to XMPP server at this interval (prevents disconnect) -->
-          <key>KeepAliveSeconds</key>
-          <integer>120</integer>
-
-          <!-- Sends a pubsub publish to a particular heartbeat node at this interval -->
-          <key>HeartbeatMinutes</key>
-          <integer>30</integer>
-
-          <!-- List of glob-like expressions defining which XMPP JIDs can converse with the server (for debugging) -->
-          <key>AllowedJIDs</key>
-          <array>
-            <!--
-            <string>*.example.com</string>
-             -->
-          </array>
+          <key>CardDAV</key>
+          <dict>
+            <key>CertificatePath</key>
+            <string>/example/contacts.cer</string>
+            <key>PrivateKeyPath</key>
+            <string>/example/contacts.pem</string>
+          </dict>
         </dict>
+
       </dict>
     </dict>
 
@@ -764,5 +738,12 @@
     </dict>
 
 
+    <key>Includes</key>
+    <array>
+        <string>%(WritablePlist)s</string>
+    </array>
+    <key>WritableConfigFile</key>
+    <string>%(WritablePlist)s</string>
+
   </dict>
 </plist>

Copied: CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/test_config.py (from rev 10929, CalendarServer/trunk/calendarserver/tools/test/test_config.py)
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/test_config.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/test_config.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -0,0 +1,220 @@
+##
+# Copyright (c) 2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.config import ConfigDict
+from calendarserver.tools.config import WritableConfig, setKeyPath, getKeyPath, flattenDictionary
+from calendarserver.tools.test.test_gateway import RunCommandTestCase
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.filepath import FilePath
+from xml.parsers.expat import ExpatError
+import plistlib
+
+PREAMBLE = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+"""
+class WritableConfigTestCase(TestCase):
+
+    def setUp(self):
+        self.configFile = self.mktemp()
+        self.fp = FilePath(self.configFile)
+
+    def test_readSuccessful(self):
+        content = """<plist version="1.0">
+    <dict>
+        <key>string</key>
+        <string>foo</string>
+    </dict>
+</plist>"""
+        self.fp.setContent(PREAMBLE + content)
+
+        config = ConfigDict()
+        writable = WritableConfig(config, self.configFile)
+        writable.read()
+        self.assertEquals(writable.currentConfigSubset, {"string":"foo"})
+
+    def test_readInvalidXML(self):
+        self.fp.setContent("invalid")
+        config = ConfigDict()
+        writable = WritableConfig(config, self.configFile)
+        self.assertRaises(ExpatError, writable.read)
+
+    def test_updates(self):
+        content = """<plist version="1.0">
+    <dict>
+        <key>key1</key>
+        <string>before</string>
+        <key>key2</key>
+        <integer>10</integer>
+    </dict>
+</plist>"""
+        self.fp.setContent(PREAMBLE + content)
+        config = ConfigDict()
+        writable = WritableConfig(config, self.configFile)
+        writable.read()
+        writable.set({"key1":"after"})
+        writable.set({"key2":15})
+        writable.set({"key2":20}) # override previous set
+        writable.set({"key3":["a", "b", "c"]})
+        self.assertEquals(writable.currentConfigSubset, {"key1":"after", "key2":20, "key3":["a", "b", "c"]})
+        writable.save()
+
+        writable2 = WritableConfig(config, self.configFile)
+        writable2.read()
+        self.assertEquals(writable2.currentConfigSubset, {"key1":"after", "key2":20, "key3":["a", "b", "c"]})
+
+    def test_convertToValue(self):
+        self.assertEquals(True, WritableConfig.convertToValue("True"))
+        self.assertEquals(False, WritableConfig.convertToValue("False"))
+        self.assertEquals(1, WritableConfig.convertToValue("1"))
+        self.assertEquals(1.2, WritableConfig.convertToValue("1.2"))
+        self.assertEquals("xyzzy", WritableConfig.convertToValue("xyzzy"))
+        self.assertEquals("xy.zzy", WritableConfig.convertToValue("xy.zzy"))
+
+
+class ConfigTestCase(RunCommandTestCase):
+
+    @inlineCallbacks
+    def test_readConfig(self):
+        """
+        Verify readConfig returns with only the writable keys
+        """
+        results = yield self.runCommand(command_readConfig,
+            script="calendarserver_config")
+
+        self.assertEquals(results["result"]["RedirectHTTPToHTTPS"], False)
+        self.assertEquals(results["result"]["EnableSearchAddressBook"], False)
+        self.assertEquals(results["result"]["EnableCalDAV"], True)
+        self.assertEquals(results["result"]["EnableCardDAV"], True)
+        self.assertEquals(results["result"]["EnableSSL"], False)
+        self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["Enabled"], False)
+        self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["CalDAV"]["CertificatePath"], "/example/calendar.cer")
+
+        # Verify not all keys are present, such as ServerRoot which is not writable
+        self.assertFalse(results["result"].has_key("ServerRoot"))
+
+    @inlineCallbacks
+    def test_writeConfig(self):
+        """
+        Verify writeConfig updates the writable plist file only
+        """
+        results = yield self.runCommand(command_writeConfig,
+            script="calendarserver_config")
+
+        self.assertEquals(results["result"]["EnableCalDAV"], False)
+        self.assertEquals(results["result"]["EnableCardDAV"], False)
+        self.assertEquals(results["result"]["EnableSSL"], True)
+        self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["Enabled"], True)
+        self.assertEquals(results["result"]["Notifications"]["Services"]["APNS"]["CalDAV"]["CertificatePath"], "/example/changed.cer")
+
+        # The static plist should still have EnableCalDAV = True
+        staticPlist = plistlib.readPlist(self.configFileName)
+        self.assertTrue(staticPlist["EnableCalDAV"])
+
+    @inlineCallbacks
+    def test_error(self):
+        """
+        Verify sending a bogus command returns an error
+        """
+        results = yield self.runCommand(command_bogusCommand,
+            script="calendarserver_config")
+        self.assertEquals(results["error"], "Unknown command 'bogus'")
+
+
+    def test_keyPath(self):
+        d = ConfigDict()
+        setKeyPath(d, "one", "A")
+        setKeyPath(d, "one", "B")
+        setKeyPath(d, "two.one", "C")
+        setKeyPath(d, "two.one", "D")
+        setKeyPath(d, "two.two", "E")
+        setKeyPath(d, "three.one.one", "F")
+        setKeyPath(d, "three.one.two", "G")
+
+        self.assertEquals(d.one, "B")
+        self.assertEquals(d.two.one, "D")
+        self.assertEquals(d.two.two, "E")
+        self.assertEquals(d.three.one.one, "F")
+        self.assertEquals(d.three.one.two, "G")
+
+        self.assertEquals(getKeyPath(d, "one"), "B")
+        self.assertEquals(getKeyPath(d, "two.one"), "D")
+        self.assertEquals(getKeyPath(d, "two.two"), "E")
+        self.assertEquals(getKeyPath(d, "three.one.one"), "F")
+        self.assertEquals(getKeyPath(d, "three.one.two"), "G")
+
+    def test_flattenDictionary(self):
+        dictionary = {
+            "one" : "A",
+            "two" : {
+                "one" : "D",
+                "two" : "E",
+            },
+            "three" : {
+                "one" : {
+                    "one" : "F",
+                    "two" : "G",
+                },
+            },
+        }
+        self.assertEquals(
+            set(list(flattenDictionary(dictionary))),
+            set([("one", "A"), ("three.one.one", "F"), ("three.one.two", "G"), ("two.one", "D"), ("two.two", "E")])
+        )
+
+
+command_readConfig = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+        <key>command</key>
+        <string>readConfig</string>
+</dict>
+</plist>
+"""
+
+command_writeConfig = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+        <key>command</key>
+        <string>writeConfig</string>
+        <key>Values</key>
+        <dict>
+            <key>EnableCalDAV</key>
+            <false/>
+            <key>EnableCardDAV</key>
+            <false/>
+            <key>EnableSSL</key>
+            <true/>
+            <key>Notifications.Services.APNS.Enabled</key>
+            <true/>
+            <key>Notifications.Services.APNS.CalDAV.CertificatePath</key>
+            <string>/example/changed.cer</string>
+        </dict>
+</dict>
+</plist>
+"""
+
+command_bogusCommand = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+        <key>command</key>
+        <string>bogus</string>
+</dict>
+</plist>
+"""

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/test_gateway.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/test_gateway.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/tools/test/test_gateway.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -29,10 +29,10 @@
 from calendarserver.tools.util import getDirectory
 
 
-class GatewayTestCase(TestCase):
+class RunCommandTestCase(TestCase):
 
     def setUp(self):
-        super(GatewayTestCase, self).setUp()
+        super(RunCommandTestCase, self).setUp()
 
         testRoot = os.path.join(os.path.dirname(__file__), "gateway")
         templateName = os.path.join(testRoot, "caldavd.plist")
@@ -42,6 +42,7 @@
 
         newConfig = template % {
             "ServerRoot" : os.path.abspath(config.ServerRoot),
+            "WritablePlist" : os.path.join(os.path.abspath(config.ConfigRoot), "caldavd-writable.plist"),
         }
         configFilePath = FilePath(os.path.join(config.ConfigRoot, "caldavd.plist"))
         configFilePath.setContent(newConfig)
@@ -71,7 +72,8 @@
         return d
 
     @inlineCallbacks
-    def runCommand(self, command, error=False):
+    def runCommand(self, command, error=False,
+        script="calendarserver_command_gateway"):
         """
         Run the given command by feeding it as standard input to
         calendarserver_command_gateway in a subprocess.
@@ -82,9 +84,9 @@
 
         sourceRoot = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
         python = sys.executable
-        gateway = os.path.join(sourceRoot, "bin", "calendarserver_command_gateway")
+        script = os.path.join(sourceRoot, "bin", script)
 
-        args = [python, gateway, "-f", self.configFileName]
+        args = [python, script, "-f", self.configFileName]
         if error:
             args.append("--error")
 
@@ -101,6 +103,9 @@
 
         returnValue(plist)
 
+
+class GatewayTestCase(RunCommandTestCase):
+
     @inlineCallbacks
     def test_getLocationList(self):
         results = yield self.runCommand(command_getLocationList)
@@ -287,6 +292,7 @@
         self.assertEquals(results["result"]["RetainDays"], 365)
 
 
+
 command_addReadProxy = """<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">

Modified: CalendarServer/branches/users/sagen/testing/calendarserver/tools/upgrade.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/calendarserver/tools/upgrade.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/calendarserver/tools/upgrade.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -15,12 +15,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from __future__ import print_function
 
 """
 This tool allows any necessary upgrade to complete, then exits.
 """
 
+from __future__ import print_function
 import os
 import sys
 import time

Modified: CalendarServer/branches/users/sagen/testing/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/branches/users/sagen/testing/conf/caldavd-apple.plist	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/conf/caldavd-apple.plist	2013-03-15 17:01:43 UTC (rev 10930)
@@ -522,5 +522,13 @@
     <key>OpenDirectoryModule</key>
     <string>calendarserver.platform.darwin.od.opendirectory</string>
 
+    <key>Includes</key>
+    <array>
+        <string>/Library/Server/Calendar and Contacts/Config/caldavd-system.plist</string>
+        <string>/Library/Server/Calendar and Contacts/Config/caldavd-user.plist</string>
+    </array>
+    <key>WritableConfigFile</key>
+    <string>/Library/Server/Calendar and Contacts/Config/caldavd-system.plist</string>
+
   </dict>
 </plist>

Deleted: CalendarServer/branches/users/sagen/testing/contrib/migration/__init__.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/contrib/migration/__init__.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/contrib/migration/__init__.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -1,15 +0,0 @@
-##
-# Copyright (c) 2010-2013 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##

Deleted: CalendarServer/branches/users/sagen/testing/contrib/migration/calendarcommonextra.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/contrib/migration/calendarcommonextra.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/contrib/migration/calendarcommonextra.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -1,187 +0,0 @@
-#!/usr/bin/env python
-#
-# CommonExtra script for calendar server.
-#
-# Copyright (c) 2012-2013 Apple Inc.  All Rights Reserved.
-#
-# IMPORTANT NOTE:  This file is licensed only for use on Apple-labeled
-# computers and is subject to the terms and conditions of the Apple
-# Software License Agreement accompanying the package this file is a
-# part of.  You may not port this file to another platform without
-# Apple's written consent.
-from __future__ import print_function
-
-import datetime
-import subprocess
-from plistlib import readPlist, writePlist
-
-LOG = "/Library/Logs/Migration/calendarmigrator.log"
-SERVER_APP_ROOT = "/Applications/Server.app/Contents/ServerRoot"
-CALENDAR_SERVER_ROOT = "/Library/Server/Calendar and Contacts"
-CALDAVD_PLIST = "%s/Config/caldavd.plist" % (CALENDAR_SERVER_ROOT,)
-SERVER_ADMIN = "%s/usr/sbin/serveradmin" % (SERVER_APP_ROOT,)
-CERT_ADMIN = "/Applications/Server.app/Contents/ServerRoot/usr/sbin/certadmin"
-PGDUMP = "%s/usr/bin/pg_dump" % (SERVER_APP_ROOT,)
-DROPDB = "%s/usr/bin/dropdb" % (SERVER_APP_ROOT,)
-POSTGRES_SERVICE_NAME = "postgres_server"
-PGSOCKETDIR = "/Library/Server/PostgreSQL For Server Services/Socket"
-USERNAME      = "caldav"
-DATABASENAME  = "caldav"
-DATADUMPFILENAME = "%s/DataDump.sql" % (CALENDAR_SERVER_ROOT,)
-
-def log(msg):
-    try:
-        timestamp = datetime.datetime.now().strftime("%b %d %H:%M:%S")
-        msg = "calendarcommonextra: %s %s" % (timestamp, msg)
-        print(msg) # so it appears in Setup.log
-        with open(LOG, 'a') as output:
-            output.write("%s\n" % (msg,)) # so it appears in our log
-    except IOError:
-        # Could not write to log
-        pass
-
-
-def startPostgres():
-    """
-    Start postgres via serveradmin
-
-    This will block until postgres is up and running
-    """
-    log("Starting %s via %s" % (POSTGRES_SERVICE_NAME, SERVER_ADMIN))
-    ret = subprocess.call([SERVER_ADMIN, "start", POSTGRES_SERVICE_NAME])
-    log("serveradmin exited with %d" % (ret,))
-
-def stopPostgres():
-    """
-    Stop postgres via serveradmin
-    """
-    log("Stopping %s via %s" % (POSTGRES_SERVICE_NAME, SERVER_ADMIN))
-    ret = subprocess.call([SERVER_ADMIN, "stop", POSTGRES_SERVICE_NAME])
-    log("serveradmin exited with %d" % (ret,))
-
-
-def dumpOldDatabase(dumpFile):
-    """
-    Use pg_dump to dump data to dumpFile
-    """
-
-    cmdArgs = [
-        PGDUMP,
-        "-h", PGSOCKETDIR,
-        "--username=%s" % (USERNAME,),
-        "--inserts",
-        "--no-privileges",
-        "--file=%s" % (dumpFile,),
-        DATABASENAME
-    ]
-    try:
-        log("Dumping data to %s" % (dumpFile,))
-        log("Executing: %s" % (" ".join(cmdArgs)))
-        out = subprocess.check_output(cmdArgs, stderr=subprocess.STDOUT)
-        log(out)
-        return True
-    except subprocess.CalledProcessError, e:
-        log(e.output)
-        return False
-
-
-def dropOldDatabase():
-    """
-    Use dropdb to delete the caldav database from the shared postgres server
-    """
-
-    cmdArgs = [
-        DROPDB,
-        "-h", PGSOCKETDIR,
-        "--username=%s" % (USERNAME,),
-        DATABASENAME
-    ]
-    try:
-        log("\nDropping %s database" % (DATABASENAME,))
-        log("Executing: %s" % (" ".join(cmdArgs)))
-        out = subprocess.check_output(cmdArgs, stderr=subprocess.STDOUT)
-        log(out)
-        return True
-    except subprocess.CalledProcessError, e:
-        log(e.output)
-        return False
-
-
-def getDefaultCert():
-    """
-    Ask certadmin for default cert
-    @returns: path to default certificate, or empty string if no default
-    @rtype: C{str}
-    """
-    child = subprocess.Popen(
-        args=[CERT_ADMIN, "--default-certificate-path"],
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-    )
-    output, error = child.communicate()
-    if child.returncode:
-        log("Error looking up default certificate (%d): %s" % (child.returncode, error))
-        return ""
-    else:
-        certPath = output.strip()
-        log("Default certificate is: %s" % (certPath,))
-        return certPath
-
-def updateSettings(settings, otherCert):
-    """
-    Replace SSL settings based on otherCert path
-    """
-    basePath = otherCert[:-len("cert.pem")]
-    log("Base path is %s" % (basePath,))
-
-    log("Setting SSLCertificate to %s" % (otherCert,))
-    settings["SSLCertificate"] = otherCert
-
-    otherChain = basePath + "chain.pem"
-    log("Setting SSLAuthorityChain to %s" % (otherChain,))
-    settings["SSLAuthorityChain"] = otherChain
-
-    otherKey = basePath + "key.pem"
-    log("Setting SSLPrivateKey to %s" % (otherKey,))
-    settings["SSLPrivateKey"] = otherKey
-
-    settings["EnableSSL"] = True
-    settings["RedirectHTTPToHTTPS"] = True
-    settings.setdefault("Authentication", {}).setdefault("Basic", {})["Enabled"] = True
-
-def setCert(plistPath, otherCert):
-    """
-    Replace SSL settings in plist at plistPath based on otherCert path
-    """
-    log("Reading plist %s" % (plistPath,))
-    plist = readPlist(plistPath)
-    log("Read in plist %s" % (plistPath,))
-
-    updateSettings(plist, otherCert)
-
-    log("Writing plist %s" % (plistPath,))
-    writePlist(plist, plistPath)
-
-def isSSLEnabled(plistPath):
-    """
-    Examine plist for EnableSSL
-    """
-    log("Reading plist %s" % (plistPath,))
-    plist = readPlist(plistPath)
-    return plist.get("EnableSSL", False)
-
-def main():
-    startPostgres()
-    if dumpOldDatabase(DATADUMPFILENAME):
-        dropOldDatabase()
-    stopPostgres()
-
-    if not isSSLEnabled(CALDAVD_PLIST):
-        defaultCertPath = getDefaultCert()
-        log("Default cert path: %s" % (defaultCertPath,))
-        if defaultCertPath:
-            setCert(CALDAVD_PLIST, defaultCertPath)
-
-
-if __name__ == "__main__":
-    main()

Deleted: CalendarServer/branches/users/sagen/testing/contrib/migration/calendardemotion.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/contrib/migration/calendardemotion.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/contrib/migration/calendardemotion.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -1,38 +0,0 @@
-#!/usr/bin/env python
-#
-# UninstallExtra script for calendar server.
-#
-# Copyright (c) 2011-2013 Apple Inc.  All Rights Reserved.
-#
-# IMPORTANT NOTE:  This file is licensed only for use on Apple-labeled
-# computers and is subject to the terms and conditions of the Apple
-# Software License Agreement accompanying the package this file is a
-# part of.  You may not port this file to another platform without
-# Apple's written consent.
-from __future__ import print_function
-
-import os
-from plistlib import readPlist, writePlist
-
-CALENDAR_SERVER_ROOT = "/Library/Server/Calendar and Contacts"
-DEST_CONFIG_DIR = "%s/Config" % (CALENDAR_SERVER_ROOT,)
-CALDAVD_PLIST = "caldavd.plist"
-
-def main():
-
-    plistPath = os.path.join(DEST_CONFIG_DIR, CALDAVD_PLIST)
-
-    if os.path.exists(plistPath):
-        try:
-            # Turn off services
-            plistData = readPlist(plistPath)
-            plistData["EnableCalDAV"] = False
-            plistData["EnableCardDAV"] = False
-            writePlist(plistData, plistPath)
-
-        except Exception, e:
-            print("Unable to disable services in %s: %s" % (plistPath, e))
-
-
-if __name__ == '__main__':
-    main()

Deleted: CalendarServer/branches/users/sagen/testing/contrib/migration/calendarmigrator.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/contrib/migration/calendarmigrator.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/contrib/migration/calendarmigrator.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -1,900 +0,0 @@
-#!/usr/bin/env python
-#
-# MigrationExtra script to maintain the enabled/disabled state of the
-# calendar server.
-#
-# This script examines the launchd preferences from the previous system
-# (also taking into account the overrides.plist) and then invokes serveradmin
-# to start/stop calendar server.
-#
-# The only argument this script currently cares about is --sourceRoot, which
-# should point to the root of the previous system.
-#
-# Copyright (c) 2005-2013 Apple Inc.  All Rights Reserved.
-#
-# IMPORTANT NOTE:  This file is licensed only for use on Apple-labeled
-# computers and is subject to the terms and conditions of the Apple
-# Software License Agreement accompanying the package this file is a
-# part of.  You may not port this file to another platform without
-# Apple's written consent.
-from __future__ import print_function
-from __future__ import with_statement
-
-import datetime
-import grp
-import optparse
-import os
-import pwd
-import shutil
-import subprocess
-import sys
-
-from plistlib import readPlist, readPlistFromString, writePlist
-
-SERVER_APP_ROOT = "/Applications/Server.app/Contents/ServerRoot"
-LOG = "/Library/Logs/Migration/calendarmigrator.log"
-CALDAVD_CONFIG_DIR = "private/etc/caldavd"
-CARDDAVD_CONFIG_DIR = "private/etc/carddavd"
-CALDAVD_PLIST = "caldavd.plist"
-CARDDAVD_PLIST = "carddavd.plist"
-NEW_SERVER_DIR = "Calendar and Contacts"
-NEW_SERVER_ROOT = "/Library/Server/" + NEW_SERVER_DIR
-NEW_CONFIG_DIR = "Library/Server/" + NEW_SERVER_DIR + "/Config"
-LOG_DIR = "var/log/caldavd"
-DITTO = "/usr/bin/ditto"
-RESOURCE_MIGRATION_TRIGGER = "trigger_resource_migration"
-
-# For looking up previous run state
-CALDAV_LAUNCHD_KEY = "org.calendarserver.calendarserver"
-CARDDAV_LAUNCHD_KEY = "org.addressbookserver.addressbookserver"
-LAUNCHD_OVERRIDES = "var/db/launchd.db/com.apple.launchd/overrides.plist"
-LAUNCHD_PREFS_DIR = "System/Library/LaunchDaemons"
-SERVER_ADMIN = "%s/usr/sbin/serveradmin" % (SERVER_APP_ROOT,)
-
-# Processed by mergePlist
-specialKeys = """
-Authentication
-BindHTTPPorts
-BindSSLPorts
-DataRoot
-DirectoryService
-DocumentRoot
-EnableSSL
-HTTPPort
-RedirectHTTPToHTTPS
-SSLAuthorityChain
-SSLCertificate
-SSLPort
-SSLPrivateKey
-""".split()
-
-# Ignored by mergePlist
-ignoredKeys = """
-EnableFindSharedReport
-EnableNotifications
-MaxAddressBookMultigetHrefs
-MaxAddressBookQueryResults
-PythonDirector
-Verbose
-""".split()
-
-
-def main():
-
-    optionParser = optparse.OptionParser()
-
-    optionParser.add_option('--purge', choices=('0', '1'),
-        metavar='[0|1]',
-        help='remove old files after migration (IGNORED)')
-
-    optionParser.add_option('--sourceRoot', type='string',
-        metavar='DIR',
-        help='path to the root of the system to migrate')
-
-    optionParser.add_option('--sourceType', type='string',
-        metavar='[System|TimeMachine]',
-        help='migration source type (IGNORED)')
-
-    optionParser.add_option('--sourceVersion', type='string',
-        metavar='10.X.X',
-        help='version number of previous system')
-
-    optionParser.add_option('--targetRoot', type='string',
-        metavar='DIR',
-        help='path to the root of the new system',
-        default='/')
-
-    optionParser.add_option('--language',
-        help='language identifier (IGNORED)',
-        default="en")
-
-    (options, args) = optionParser.parse_args()
-    log("Options: %s" % (options,))
-
-    if options.sourceRoot and options.sourceVersion:
-
-        if os.path.exists(options.sourceRoot):
-
-            enableCalDAV, enableCardDAV = examineRunState(options)
-
-            # Pull values out of previous plists
-            (
-                oldServerRootValue,
-                oldCalDocumentRootValue,
-                oldCalDataRootValue,
-                oldABDocumentRootValue,
-                uid,
-                gid
-            ) = examinePreviousSystem(
-                options.sourceRoot,
-                options.targetRoot
-            )
-
-            # Copy data as needed
-            (
-                newServerRoot,
-                newServerRootValue,
-                newDataRootValue
-            ) = relocateData(
-                options.sourceRoot,
-                options.targetRoot,
-                options.sourceVersion,
-                oldServerRootValue,
-                oldCalDocumentRootValue,
-                oldCalDataRootValue,
-                oldABDocumentRootValue,
-                uid,
-                gid
-            )
-
-            # Combine old and new plists
-            migrateConfiguration(
-                options,
-                newServerRootValue,
-                newDataRootValue,
-                enableCalDAV,
-                enableCardDAV
-            )
-
-            # Create log directory
-            try:
-                logDir = os.path.join(options.targetRoot, LOG_DIR)
-                os.mkdir(logDir, 0755)
-            except OSError:
-                # Already exists
-                pass
-            # Set ownership
-            os.chown(logDir, uid, gid)
-
-            # Trigger migration of locations and resources from OD
-            triggerResourceMigration(newServerRoot)
-
-            # TODO: instead of starting now, leave breadcrumbs for
-            # the commonextra to start the service, so that data can
-            # be dumped from the old Postgres to a file which will
-            # be executed by calendar server when it next starts up.
-
-            # setRunState(options, enableCalDAV, enableCardDAV)
-
-    else:
-        log("ERROR: --sourceRoot and --sourceVersion must be specified")
-        sys.exit(1)
-
-
-def examineRunState(options):
-    """
-    Try to determine whether the CalDAV and CardDAV services were running in
-    previous system.
-
-    @return: a tuple of booleans: whether CalDAV was enabled, and whether
-    CardDAV was enabled
-    """
-
-    enableCalDAV = None
-    enableCardDAV = None
-
-    try:
-        disabled = isServiceDisabled(options.sourceRoot, CALDAV_LAUNCHD_KEY)
-        enableCalDAV = not disabled
-        log("Calendar service '%s' was previously %s" %
-            (CALDAV_LAUNCHD_KEY, "disabled" if disabled else "enabled"))
-    except ServiceStateError, e:
-        log("Couldn't determine previous state of calendar service '%s': %s" %
-            (CALDAV_LAUNCHD_KEY, e))
-
-    try:
-        disabled = isServiceDisabled(options.sourceRoot, CARDDAV_LAUNCHD_KEY)
-        enableCardDAV = not disabled
-        log("Addressbook service '%s' was previously %s" %
-            (CARDDAV_LAUNCHD_KEY, "disabled" if disabled else "enabled"))
-    except ServiceStateError, e:
-        log("Couldn't determine previous state of addressbook service '%s': %s" %
-            (CARDDAV_LAUNCHD_KEY, e))
-
-    if enableCalDAV:
-        # Check previous plist in case previous system was Lion, since there
-        # is now only one launchd key for both services
-        oldCalDAVPlistPath = os.path.join(options.sourceRoot,
-            CALDAVD_CONFIG_DIR, CALDAVD_PLIST)
-        if os.path.exists(oldCalDAVPlistPath):
-            log("Examining previous caldavd.plist for EnableCalDAV and EnableCardDAV: %s" % (oldCalDAVPlistPath,))
-            oldCalDAVDPlist = readPlist(oldCalDAVPlistPath)
-            if "EnableCalDAV" in oldCalDAVDPlist:
-                enableCalDAV = oldCalDAVDPlist["EnableCalDAV"]
-                log("Based on caldavd.plist, setting EnableCalDAV to %s" % (enableCalDAV,))
-            if "EnableCardDAV" in oldCalDAVDPlist:
-                enableCardDAV = oldCalDAVDPlist["EnableCardDAV"]
-                log("Based on caldavd.plist, setting EnableCardDAV to %s" % (enableCardDAV,))
-
-    # A value of None means we weren't able to determine, so default to off
-    if enableCalDAV is None:
-        enableCalDAV = False
-    if enableCardDAV is None:
-        enableCardDAV = False
-
-    return (enableCalDAV, enableCardDAV)
-
-
-def setRunState(options, enableCalDAV, enableCardDAV):
-    """
-    Use serveradmin to launch the service if needed.
-    """
-
-    if enableCalDAV or enableCardDAV:
-        serviceName = "calendar" if enableCalDAV else "addressbook"
-        log("Starting service via serveradmin start %s" % (serviceName,))
-        ret = subprocess.call([SERVER_ADMIN, "start", serviceName])
-        log("serveradmin exited with %d" % (ret,))
-
-
-def isServiceDisabled(source, service, launchdOverrides=LAUNCHD_OVERRIDES,
-    launchdPrefsDir=LAUNCHD_PREFS_DIR):
-    """
-    Returns whether or not a service is disabled
-
-    @param source: System root to examine
-    @param service: launchd key representing service
-    @return: True if service is disabled, False if enabled
-    """
-
-    overridesPath = os.path.join(source, launchdOverrides)
-    if os.path.isfile(overridesPath):
-        try:
-            overrides = readPlist(overridesPath)
-        except Exception, e:
-            raise ServiceStateError("Could not parse %s : %s" %
-                (overridesPath, str(e)))
-
-        try:
-            return overrides[service]['Disabled']
-        except KeyError:
-            # Key is not in the overrides.plist, continue on
-            pass
-
-    prefsPath = os.path.join(source, launchdPrefsDir, "%s.plist" % service)
-    if os.path.isfile(prefsPath):
-        try:
-            prefs = readPlist(prefsPath)
-        except Exception, e:
-            raise ServiceStateError("Could not parse %s : %s" %
-                (prefsPath, str(e)))
-        try:
-            return prefs['Disabled']
-        except KeyError:
-            return False
-
-    raise ServiceStateError("Neither %s nor %s exist" %
-        (overridesPath, prefsPath))
-
-
-class ServiceStateError(Exception):
-    """
-    Could not determine service state
-    """
-
-
-
-def migrateConfiguration(options, newServerRootValue, newDataRootValue, enableCalDAV, enableCardDAV):
-    """
-    Copy files/directories/symlinks from previous system's /etc/caldavd
-    and /etc/carddavd
-
-    Skips anything ending in ".default".
-    Regular files overwrite copies in new system.
-    Directories and symlinks only copied over if they don't overwrite anything.
-    """
-
-    newConfigDir = os.path.join(options.targetRoot, NEW_CONFIG_DIR)
-    newConfigFile = os.path.join(newConfigDir, CALDAVD_PLIST)
-
-    # Create config directory if it doesn't exist
-    if not os.path.exists(newConfigDir):
-        os.mkdir(newConfigDir)
-
-    defaultConfig = os.path.join(SERVER_APP_ROOT, CALDAVD_CONFIG_DIR, CALDAVD_PLIST)
-    if os.path.exists(defaultConfig) and not os.path.exists(newConfigFile):
-        log("Copying default config file %s to %s" % (defaultConfig, newConfigFile))
-        shutil.copy2(defaultConfig, newConfigFile)
-
-    for configDir in (NEW_CONFIG_DIR, CALDAVD_CONFIG_DIR, CARDDAVD_CONFIG_DIR):
-
-        oldConfigDir = os.path.join(options.sourceRoot, configDir)
-        if not os.path.exists(oldConfigDir):
-            log("Old configuration directory does not exist: %s" % (oldConfigDir,))
-            continue
-
-        log("Copying configuration files from %s to %s" % (oldConfigDir, newConfigDir))
-
-        for name in os.listdir(oldConfigDir):
-
-            if not (name.endswith(".default") or name in (CALDAVD_PLIST, CARDDAVD_PLIST)):
-
-                oldPath = os.path.join(oldConfigDir, name)
-                newPath = os.path.join(newConfigDir, name)
-
-                if os.path.islink(oldPath) and not os.path.exists(newPath):
-                    # Recreate the symlink if it won't overwrite an existing file
-                    link = os.readlink(oldPath)
-                    log("Symlinking %s to %s" % (newPath, link))
-                    os.symlink(link, newPath)
-
-                elif os.path.isfile(oldPath):
-                    # Copy the file over, overwriting copy in newConfigDir
-                    log("Copying file %s to %s" % (oldPath, newConfigDir))
-                    shutil.copy2(oldPath, newConfigDir)
-
-                elif os.path.isdir(oldPath) and not os.path.exists(newPath):
-                    # Copy the dir over, but only if new one doesn't exist
-                    log("Copying directory %s to %s" % (oldPath, newPath))
-                    shutil.copytree(oldPath, newPath, symlinks=True)
-
-
-    # Migrate certain settings from the old plists to new:
-
-    oldCalDAVPlistPath = os.path.join(options.sourceRoot, CALDAVD_CONFIG_DIR,
-        CALDAVD_PLIST)
-    if os.path.exists(oldCalDAVPlistPath):
-        oldCalDAVDPlist = readPlist(oldCalDAVPlistPath)
-    else:
-        oldCalDAVDPlist = { }
-
-    oldCardDAVDPlistPath = os.path.join(options.sourceRoot, CARDDAVD_CONFIG_DIR,
-        CARDDAVD_PLIST)
-    if os.path.exists(oldCardDAVDPlistPath):
-        oldCardDAVDPlist = readPlist(oldCardDAVDPlistPath)
-    else:
-        oldCardDAVDPlist = { }
-
-    if os.path.exists(newConfigFile):
-        newCalDAVDPlist = readPlist(newConfigFile)
-    else:
-        newCalDAVDPlist = { }
-
-    log("Processing %s and %s" % (oldCalDAVPlistPath, oldCardDAVDPlistPath))
-    adminChanges = mergePlist(oldCalDAVDPlist, oldCardDAVDPlist, newCalDAVDPlist)
-
-    newCalDAVDPlist["ServerRoot"] = newServerRootValue
-    newCalDAVDPlist["DocumentRoot"] = "Documents"
-    newCalDAVDPlist["DataRoot"] = newDataRootValue
-
-    newCalDAVDPlist["EnableCalDAV"] = enableCalDAV
-    newCalDAVDPlist["EnableCardDAV"] = enableCardDAV
-
-    log("Writing %s" % (newConfigFile,))
-    writePlist(newCalDAVDPlist, newConfigFile)
-
-    for key, value in adminChanges:
-        log("Setting %s to %s via serveradmin..." % (key, value))
-        ret = subprocess.call([SERVER_ADMIN, "settings", "calendar:%s=%s" % (key, value)])
-        log("serveradmin exited with %d" % (ret,))
-
-
-
-def mergePlist(caldav, carddav, combined):
-
-    adminChanges = []
-
-    # Copy all non-ignored keys
-    for key in carddav:
-        if key not in ignoredKeys and key not in specialKeys:
-            combined[key] = carddav[key]
-    for key in caldav:
-        if key not in ignoredKeys and key not in specialKeys:
-            combined[key] = caldav[key]
-
-    # Copy all "Authentication" sub-keys
-    if "Authentication" in caldav:
-        if "Authentication" not in combined:
-            combined["Authentication"] = { }
-        for key in caldav["Authentication"]:
-            combined["Authentication"][key] = caldav["Authentication"][key]
-
-        # Reset the wiki settings since URL is only used wieh LionCompatibility
-        combined["Authentication"]["Wiki"] = { "Enabled" : True }
-
-    # Strip out any unknown params from the DirectoryService:
-    if "DirectoryService" in caldav:
-        combined["DirectoryService"] = caldav["DirectoryService"]
-        for key in combined["DirectoryService"]["params"].keys():
-            if key in ("requireComputerRecord",):
-                del combined["DirectoryService"]["params"][key]
-
-    # Disable XMPPNotifier now that we're directly talking to APNS
-    try:
-        XMPPNotifier = caldav["Notifications"]["Services"]["XMPPNotifier"]
-        if XMPPNotifier["Enabled"]:
-            XMPPNotifier["Enabled"] = False
-    except KeyError:
-        pass
-
-    # If XMPP was also previously being routed to APNS, enable APNS
-    EnableAPNS = False
-    try:
-        if caldav["Notifications"]["Services"]["XMPPNotifier"]["CalDAV"]["APSBundleID"]:
-            EnableAPNS = True
-    except KeyError:
-        pass
-    try:
-        if caldav["Notifications"]["Services"]["XMPPNotifier"]["CardDAV"]["APSBundleID"]:
-            EnableAPNS = True
-    except KeyError:
-        pass
-    if EnableAPNS:
-        adminChanges.append(["EnableAPNS", "yes"])
-
-    # Merge ports
-    if not caldav.get("HTTPPort", 0):
-        caldav["HTTPPort"] = 8008
-    if not carddav.get("HTTPPort", 0):
-        carddav["HTTPPort"] = 8800
-    if not caldav.get("SSLPort", 0):
-        caldav["SSLPort"] = 8443
-    if not carddav.get("SSLPort", 0):
-        carddav["SSLPort"] = 8843
-
-    for portType in ["HTTPPort", "SSLPort"]:
-        bindPorts = list(set(caldav.get("Bind%ss" % (portType,), [])).union(set(carddav.get("Bind%ss" % (portType,), []))))
-        for prev in (carddav, caldav):
-            port = prev.get(portType, 0)
-            if port and port not in bindPorts:
-                bindPorts.append(port)
-        bindPorts.sort()
-        combined["Bind%ss" % (portType,)] = bindPorts
-
-    combined["HTTPPort"] = caldav["HTTPPort"]
-    combined["SSLPort"] = caldav["SSLPort"]
-
-    # Was SSL enabled?
-    sslAuthorityChain = ""
-    sslCertificate = ""
-    sslPrivateKey = ""
-    enableSSL = False
-    for prev in (carddav, caldav):
-        if (prev["SSLPort"] and prev.get("SSLCertificate", "")):
-            sslAuthorityChain = prev.get("SSLAuthorityChain", "")
-            sslCertificate = prev.get("SSLCertificate", "")
-            sslPrivateKey = prev.get("SSLPrivateKey", "")
-            enableSSL = True
-
-    combined["SSLAuthorityChain"] = sslAuthorityChain
-    combined["SSLCertificate"] = sslCertificate
-    combined["SSLPrivateKey"] = sslPrivateKey
-    combined["EnableSSL"] = enableSSL
-
-    # If SSL is enabled, redirect HTTP to HTTPS.
-    combined["RedirectHTTPToHTTPS"] = enableSSL
-
-    # New DBType value indicating we launch our own Postgres
-    combined["DBType"] = ""
-
-    # No DSN value since we launch our own Postgres
-    combined["DSN"] = ""
-
-    # Path to SQL file to import previous data from
-    combined["DBImportFile"] = "/Library/Server/Calendar and Contacts/DataDump.sql"
-
-    # ConfigRoot is now always "Config"
-    combined["ConfigRoot"] = "Config"
-
-    # Remove RunRoot and PIDFile keys so they use the new defaults
-    try:
-        del combined["RunRoot"]
-    except:
-        pass
-    try:
-        del combined["PIDFile"]
-    except:
-        pass
-
-    return adminChanges
-
-
-def log(msg):
-    try:
-        timestamp = datetime.datetime.now().strftime("%b %d %H:%M:%S")
-        msg = "calendarmigrator: %s %s" % (timestamp, msg)
-        print(msg) # so it appears in Setup.log
-        with open(LOG, 'a') as output:
-            output.write("%s\n" % (msg,)) # so it appears in our log
-    except IOError:
-        # Could not write to log
-        pass
-
-def examinePreviousSystem(sourceRoot, targetRoot, diskAccessor=None):
-    """
-    Examines the old caldavd.plist and carddavd.plist to see where data
-    lives in the previous system.
-    """
-
-    if diskAccessor is None:
-        diskAccessor = DiskAccessor()
-
-    oldServerRootValue = None
-    oldCalDocumentRootValue = None
-    oldCalDataRootValue = None
-    oldABDocumentRootValue = None
-
-    uid = pwd.getpwnam("calendar").pw_uid
-    gid = grp.getgrnam("calendar").gr_gid
-
-    # Try and read old caldavd.plist
-    oldCalConfigDir = os.path.join(sourceRoot, CALDAVD_CONFIG_DIR)
-    oldCalPlistPath = os.path.join(oldCalConfigDir, CALDAVD_PLIST)
-    if diskAccessor.exists(oldCalPlistPath):
-        contents = diskAccessor.readFile(oldCalPlistPath)
-        oldCalPlist = readPlistFromString(contents)
-        log("Found previous caldavd plist at %s" % (oldCalPlistPath,))
-
-        oldServerRootValue = oldCalPlist.get("ServerRoot", None)
-        oldCalDocumentRootValue = oldCalPlist.get("DocumentRoot", None)
-        oldCalDataRootValue = oldCalPlist.get("DataRoot", None)
-
-    else:
-        log("Can't find previous calendar plist at %s" % (oldCalPlistPath,))
-        oldCalPlist = None
-
-    # Try and read old carddavd.plist
-    oldABConfigDir = os.path.join(sourceRoot, CARDDAVD_CONFIG_DIR)
-    oldABPlistPath = os.path.join(oldABConfigDir, CARDDAVD_PLIST)
-    if diskAccessor.exists(oldABPlistPath):
-        contents = diskAccessor.readFile(oldABPlistPath)
-        oldABPlist = readPlistFromString(contents)
-        log("Found previous carddavd plist at %s" % (oldABPlistPath,))
-
-        oldABDocumentRootValue = oldABPlist.get("DocumentRoot", None)
-    else:
-        log("Can't find previous carddavd plist at %s" % (oldABPlistPath,))
-        oldABPlist = None
-
-    return (
-        oldServerRootValue,
-        oldCalDocumentRootValue,
-        oldCalDataRootValue,
-        oldABDocumentRootValue,
-        uid,
-        gid
-    )
-
-
-def relocateData(sourceRoot, targetRoot, sourceVersion, oldServerRootValue,
-    oldCalDocumentRootValue, oldCalDataRootValue, oldABDocumentRootValue,
-    uid, gid, diskAccessor=None):
-    """
-    Copy data from sourceRoot to targetRoot, except when data is on another
-    volume in which case we just refer to it there.
-    """
-
-    if diskAccessor is None:
-        diskAccessor = DiskAccessor()
-
-    log("RelocateData: sourceRoot=%s, targetRoot=%s, oldServerRootValue=%s, oldCalDocumentRootValue=%s, oldCalDataRootValue=%s, oldABDocumentRootValue=%s, uid=%d, gid=%d" % (sourceRoot, targetRoot, oldServerRootValue, oldCalDocumentRootValue, oldCalDataRootValue, oldABDocumentRootValue, uid, gid))
-
-    newServerRootValue = "/Library/Server/Calendar and Contacts"
-    newServerRoot = absolutePathWithRoot(targetRoot, newServerRootValue)
-
-    if sourceVersion < "10.7":
-        oldCalDocumentRootValueProcessed = oldCalDocumentRootValue
-        oldCalDataRootValueProcessed = oldCalDataRootValue
-
-    else:
-        # If there was an old ServerRoot value, process DocumentRoot and
-        # DataRoot because those could be relative to ServerRoot
-
-        if sourceVersion < "10.8":
-            # DocumentRoot and DataRoot are both relative to ServerRoot
-            oldCalDocumentRootValueProcessed = os.path.join(oldServerRootValue,
-                oldCalDocumentRootValue)
-            oldCalDataRootValueProcessed = os.path.join(oldServerRootValue,
-                oldCalDataRootValue)
-        else:
-            # DocumentRoot is relative to DataRoot, DataRoot is relative to ServerRoot
-            oldCalDataRootValueProcessed = os.path.join(oldServerRootValue,
-                oldCalDataRootValue)
-            oldCalDocumentRootValueProcessed = os.path.join(oldCalDataRootValueProcessed,
-                oldCalDocumentRootValue)
-
-
-    # Set default values for these, possibly overridden below:
-    newDataRootValue = "Data"
-    newDataRoot = absolutePathWithRoot(
-        targetRoot,
-        os.path.join(newServerRootValue, newDataRootValue)
-    )
-    newDocumentRootValue = "Documents"
-    newDocumentRoot = os.path.join(newDataRoot, newDocumentRootValue)
-
-    if sourceVersion < "10.7":
-        # Before 10.7 there was no ServerRoot; DocumentRoot and DataRoot were separate.
-        # Reconfigure so DocumentRoot is under DataRoot is under ServerRoot.  DataRoot
-        # will be /Library/Server/Calendar and Contacts/Data unless old DocumentRoot was on
-        # an external volume, in which case that becomes the new DataRoot and DocumentRoot
-        # moves under DataRoot.
-        # /Library/Server/Calendar and Contacts will be new ServerRoot no matter what.
-
-        if oldCalDocumentRootValueProcessed:
-            if oldCalDocumentRootValueProcessed.startswith("/Volumes/"): # external volume
-                # The old external calendar DocumentRoot becomes the new DataRoot
-                newDataRoot = newDataRootValue = os.path.join(os.path.dirname(oldCalDocumentRootValue.rstrip("/")), "Calendar and Contacts Data")
-                newDocumentRoot = os.path.join(newDataRoot, newDocumentRootValue)
-                # Move aside whatever is there
-                if diskAccessor.exists(newDataRoot):
-                    renameTo = nextAvailable(newDataRoot, "bak", diskAccessor=diskAccessor)
-                    diskAccessor.rename(newDataRoot, renameTo)
-
-                if diskAccessor.exists(absolutePathWithRoot(sourceRoot, oldCalDataRootValueProcessed)):
-                    diskAccessor.ditto(
-                        absolutePathWithRoot(sourceRoot, oldCalDataRootValueProcessed),
-                        newDataRoot
-                    )
-                else:
-                    diskAccessor.mkdir(newDataRoot)
-
-                # Move old DocumentRoot under new DataRoot
-                diskAccessor.rename(oldCalDocumentRootValue, newDocumentRoot)
-                diskAccessor.chown(newDataRoot, uid, gid, recursive=True)
-
-            else: # The old calendar DocumentRoot is not external
-                if oldCalDataRootValueProcessed:
-                    if diskAccessor.exists(absolutePathWithRoot(sourceRoot,
-                        oldCalDataRootValueProcessed)):
-                        diskAccessor.ditto(
-                            absolutePathWithRoot(sourceRoot, oldCalDataRootValueProcessed),
-                            newDataRoot
-                        )
-                if diskAccessor.exists(absolutePathWithRoot(sourceRoot,
-                    oldCalDocumentRootValueProcessed)):
-                    diskAccessor.ditto(
-                        absolutePathWithRoot(sourceRoot, oldCalDocumentRootValueProcessed),
-                        newDocumentRoot
-                    )
-
-        # Old AddressBook DocumentRoot
-        if oldABDocumentRootValue:
-            newAddressBooks = os.path.join(newDocumentRoot, "addressbooks")
-            if oldABDocumentRootValue.startswith("/Volumes/"): # external volume
-                diskAccessor.ditto(
-                    os.path.join(oldABDocumentRootValue, "addressbooks"),
-                    newAddressBooks
-                )
-            elif diskAccessor.exists(
-                absolutePathWithRoot(sourceRoot, oldABDocumentRootValue)
-            ):
-                diskAccessor.ditto(
-                    absolutePathWithRoot(
-                        sourceRoot,
-                        os.path.join(oldABDocumentRootValue, "addressbooks")
-                    ),
-                    os.path.join(newDocumentRoot, "addressbooks")
-                )
-
-
-    elif sourceVersion < "10.8":
-        # Before 10.8, DocumentRoot and DataRoot were relative to ServerRoot
-
-        if oldServerRootValue:
-            if oldServerRootValue.rstrip("/").startswith("/Volumes/"): # external volume
-                log("Using external calendar server root: %s" % (oldServerRootValue,))
-                # ServerRoot needs to be /Library/Server/Calendar and Contacts
-                # Since DocumentRoot is now relative to DataRoot, move DocumentRoot into DataRoot
-                newDataRoot = newDataRootValue = os.path.join(oldServerRootValue, "Data")
-                if not diskAccessor.exists(newDataRoot):
-                    diskAccessor.mkdir(newDataRoot)
-                newDocumentRoot = os.path.join(newDataRootValue, "Documents")
-                if not diskAccessor.exists(newDocumentRoot):
-                    if diskAccessor.exists(os.path.join(oldServerRootValue, "Documents")):
-                        diskAccessor.rename(os.path.join(oldServerRootValue, "Documents"),
-                            newDocumentRoot)
-                    else:
-                        diskAccessor.mkdir(newDocumentRoot)
-            elif diskAccessor.exists(absolutePathWithRoot(sourceRoot, oldServerRootValue)):
-                log("Copying calendar server root: %s" % (newServerRoot,))
-                diskAccessor.ditto(
-                    absolutePathWithRoot(sourceRoot, oldServerRootValue),
-                    newServerRoot
-                )
-                newDataRoot = os.path.join(newServerRoot, "Data")
-                if not diskAccessor.exists(newDataRoot):
-                    diskAccessor.mkdir(newDataRoot)
-                newDocumentRoot = os.path.join(newDataRoot, "Documents")
-                if not diskAccessor.exists(newDocumentRoot):
-                    if diskAccessor.exists(os.path.join(newServerRoot, "Documents")):
-                        log("Moving Documents into Data root: %s" % (newDataRoot,))
-                        diskAccessor.rename(os.path.join(newServerRoot, "Documents"),
-                            newDocumentRoot)
-                    else:
-                        diskAccessor.mkdir(newDocumentRoot)
-            else:
-                if not diskAccessor.exists(newServerRoot):
-                    log("Creating new calendar server root: %s" % (newServerRoot,))
-                    diskAccessor.mkdir(newServerRoot)
-                newDataRoot = os.path.join(newServerRoot, "Data")
-                if not diskAccessor.exists(newDataRoot):
-                    log("Creating new data root: %s" % (newDataRoot,))
-                    diskAccessor.mkdir(newDataRoot)
-                newDocumentRoot = os.path.join(newDataRoot, "Documents")
-                if not diskAccessor.exists(newDocumentRoot):
-                    log("Creating new document root: %s" % (newDocumentRoot,))
-                    diskAccessor.mkdir(newDocumentRoot)
-
-
-    else: # 10.8 -> 10.8
-
-        if oldServerRootValue:
-            if oldServerRootValue.rstrip("/").startswith("/Volumes/"): # external volume
-                log("Using external calendar server root: %s" % (oldServerRootValue,))
-            elif diskAccessor.exists(absolutePathWithRoot(sourceRoot, oldServerRootValue)):
-                log("Copying calendar server root: %s" % (newServerRoot,))
-                diskAccessor.ditto(
-                    absolutePathWithRoot(sourceRoot, oldServerRootValue),
-                    newServerRoot
-                )
-            else:
-                log("Creating new calendar server root: %s" % (newServerRoot,))
-                diskAccessor.mkdir(newServerRoot)
-                newDataRoot = os.path.join(newServerRoot, "Data")
-                diskAccessor.mkdir(newDataRoot)
-                newDocumentRoot = os.path.join(newDataRoot, "Documents")
-                diskAccessor.mkdir(newDocumentRoot)
-
-    if not diskAccessor.exists(newServerRoot):
-        diskAccessor.mkdir(newServerRoot)
-    diskAccessor.chown(newServerRoot, uid, gid, recursive=True)
-
-    newServerRootValue, newDataRootValue = relativize(newServerRootValue,
-        newDataRootValue)
-    newDataRootValue, newDocumentRootValue = relativize(newDataRootValue,
-        newDocumentRootValue)
-
-
-    return (
-        newServerRoot,
-        newServerRootValue,
-        newDataRootValue
-    )
-
-
-def triggerResourceMigration(newServerRoot):
-    """
-    Leave a file in the server root to act as a signal that the server
-    should migrate locations and resources from OD when it starts up.
-    """
-    triggerPath = os.path.join(newServerRoot, RESOURCE_MIGRATION_TRIGGER)
-    if not os.path.exists(newServerRoot):
-        log("New server root directory doesn't exist: %s" % (newServerRoot,))
-        return
-
-    if not os.path.exists(triggerPath):
-        # Create an empty trigger file
-        log("Creating resource migration trigger file: %s" % (triggerPath,))
-        open(triggerPath, "w").close()
-
-
-def relativize(parent, child):
-    """
-    If child is really a child of parent, make child relative to parent.
-    """
-    if child.startswith(parent):
-        parent = parent.rstrip("/")
-        child = child[len(parent):].strip("/")
-    return parent.rstrip("/"), child.rstrip("/")
-
-
-def absolutePathWithRoot(root, path):
-    """
-    Combine root and path as long as path does not start with /Volumes/
-    """
-    if path.startswith("/Volumes/"):
-        return path
-    else:
-        path = path.strip("/")
-        return os.path.join(root, path)
-
-
-def nextAvailable(path, ext, diskAccessor=None):
-    """
-    If path.ext doesn't exist, return path.ext.  Otherwise return the first path name
-    following the path.N.ext pattern that doesn't exist, where N starts at 1
-    and increments until a non-existent path name is determined.
-
-    @param path: path to examine
-    @type path: C{str}
-    @param ext: filename extension to append (don't include ".")
-    @type ext: C{str}
-    @returns: non-existent path name
-    @rtype: C{str}
-    """
-
-    if diskAccessor is None:
-        diskAccessor = DiskAccessor()
-
-    newPath = "%s.%s" % (path, ext)
-    if not diskAccessor.exists(newPath):
-        return newPath
-
-    i = 1
-    while(True):
-        newPath = "%s.%d.%s" % (path, i, ext)
-        if not diskAccessor.exists(newPath):
-            return newPath
-        i += 1
-
-
-class DiskAccessor(object):
-    """
-    A wrapper around various disk access methods so that unit tests can easily
-    replace these with a stub that doesn't actually require disk access.
-    """
-
-    def exists(self, path):
-        return os.path.exists(path)
-
-    def readFile(self, path):
-        input = file(path)
-        contents = input.read()
-        input.close()
-        return contents
-
-    def mkdir(self, path):
-        if not self.exists(path):
-            return os.mkdir(path)
-        else:
-            return
-
-    def rename(self, before, after):
-        log("Renaming: %s to %s" % (before, after))
-        try:
-            return os.rename(before, after)
-        except OSError:
-            # Can't rename because it's cross-volume; must copy/delete
-            self.ditto(before, after)
-            return os.remove(before)
-
-    def isfile(self, path):
-        return os.path.isfile(path)
-
-    def symlink(self, orig, link):
-        return os.symlink(orig, link)
-
-    def chown(self, path, uid, gid, recursive=False):
-        os.chown(path, uid, gid)
-        if recursive:
-            for root, dirs, files in os.walk(path, followlinks=True):
-                for name in dirs:
-                    os.chown(os.path.join(root, name), uid, gid)
-                for name in files:
-                    os.chown(os.path.join(root, name), uid, gid)
-
-
-    def walk(self, path, followlinks=True):
-        return os.walk(path, followlinks=followlinks)
-
-    def listdir(self, path):
-        return list(os.listdir(path))
-
-    def ditto(self, src, dest):
-        log("Copying with ditto: %s to %s" % (src, dest))
-        return subprocess.call([DITTO, src, dest])
-
-
-if __name__ == '__main__':
-    main()

Deleted: CalendarServer/branches/users/sagen/testing/contrib/migration/calendarpromotion.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/contrib/migration/calendarpromotion.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/contrib/migration/calendarpromotion.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -1,102 +0,0 @@
-#!/usr/bin/env python
-#
-# PromotionExtra script for calendar server.
-#
-# Copyright (c) 2011-2013 Apple Inc.  All Rights Reserved.
-#
-# IMPORTANT NOTE:  This file is licensed only for use on Apple-labeled
-# computers and is subject to the terms and conditions of the Apple
-# Software License Agreement accompanying the package this file is a
-# part of.  You may not port this file to another platform without
-# Apple's written consent.
-from __future__ import print_function
-
-import os
-import shutil
-from pwd import getpwnam
-from grp import getgrnam
-from plistlib import readPlist, writePlist
-
-SRC_CONFIG_DIR = "/Applications/Server.app/Contents/ServerRoot/private/etc/caldavd"
-CALENDAR_SERVER_ROOT = "/Library/Server/Calendar and Contacts"
-DEST_CONFIG_DIR = "%s/Config" % (CALENDAR_SERVER_ROOT,)
-DEST_DATA_DIR = "%s/Data" % (CALENDAR_SERVER_ROOT,)
-CALDAVD_PLIST = "caldavd.plist"
-USER_NAME = "calendar"
-GROUP_NAME = "calendar"
-LOG_DIR = "/var/log/caldavd"
-RUN_DIR = "/var/run/caldavd"
-
-
-def updatePlist(plistData):
-    """
-    Update the passed-in plist data with new values for disabling the XMPPNotifier,
-    to set DBType to empty string indicating we'll be starting our own Postgres server,
-    and to specify the new location for ConfigRoot ("Config" directory beneath ServerRoot).
-
-    @param plistData: the plist data to update in place
-    @type plistData: C{dict}
-    """
-    try:
-        if plistData["Notifications"]["Services"]["XMPPNotifier"]["Enabled"]:
-            plistData["Notifications"]["Services"]["XMPPNotifier"]["Enabled"] = False
-    except KeyError:
-        pass
-    plistData["DBType"] = ""
-    plistData["DSN"] = ""
-    plistData["ConfigRoot"] = "Config"
-    plistData["DBImportFile"] = "/Library/Server/Calendar and Contacts/DataDump.sql"
-    # Remove RunRoot and PIDFile keys so they use the new defaults
-    try:
-        del plistData["RunRoot"]
-    except:
-        pass
-    try:
-        del plistData["PIDFile"]
-    except:
-        pass
-
-
-
-def main():
-
-    for dirName in (
-        CALENDAR_SERVER_ROOT,
-        DEST_CONFIG_DIR,
-        DEST_DATA_DIR,
-        LOG_DIR,
-        RUN_DIR
-    ):
-        try:
-            os.mkdir(dirName)
-        except OSError:
-            # Already exists
-            pass
-
-        try:
-            uid = getpwnam(USER_NAME).pw_uid
-            gid = getgrnam(GROUP_NAME).gr_gid
-            os.chown(dirName, uid, gid)
-        except Exception, e:
-            print("Unable to chown %s: %s" % (dirName, e))
-
-
-    plistPath = os.path.join(DEST_CONFIG_DIR, CALDAVD_PLIST)
-
-    if os.path.exists(plistPath):
-        try:
-            plistData = readPlist(plistPath)
-            updatePlist(plistData)
-            writePlist(plistData, plistPath)
-
-        except Exception, e:
-            print("Unable to disable update values in %s: %s" % (plistPath, e))
-
-    else:
-        # Copy configuration
-        srcPlistPath = os.path.join(SRC_CONFIG_DIR, CALDAVD_PLIST)
-        shutil.copy(srcPlistPath, DEST_CONFIG_DIR)
-
-
-if __name__ == '__main__':
-    main()

Copied: CalendarServer/branches/users/sagen/testing/doc/calendarserver_config.8 (from rev 10929, CalendarServer/trunk/doc/calendarserver_config.8)
===================================================================
--- CalendarServer/branches/users/sagen/testing/doc/calendarserver_config.8	                        (rev 0)
+++ CalendarServer/branches/users/sagen/testing/doc/calendarserver_config.8	2013-03-15 17:01:43 UTC (rev 10930)
@@ -0,0 +1,59 @@
+.\"
+.\" Copyright (c) 2006-2013 Apple Inc. All rights reserved.
+.\"
+.\" Licensed under the Apache License, Version 2.0 (the "License");
+.\" you may not use this file except in compliance with the License.
+.\" You may obtain a copy of the License at
+.\"
+.\"     http://www.apache.org/licenses/LICENSE-2.0
+.\"
+.\" Unless required by applicable law or agreed to in writing, software
+.\" distributed under the License is distributed on an "AS IS" BASIS,
+.\" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.\" See the License for the specific language governing permissions and
+.\" limitations under the License.
+.\"
+.\" The following requests are required for all man pages.
+.Dd January 14, 2013
+.Dt CALENDARSERVER_CONFIG 8
+.Os
+.Sh NAME
+.Nm calendarserver_config
+.Nd Calendar Server Configuration Utility
+.Sh SYNOPSIS
+.Nm
+.Op Fl -config Ar file
+.Op key ...
+.Sh DESCRIPTION
+.Nm
+stores and retrieves configuration values for calendar server.  It's primary
+purpose is to carry out operations on behalf of the Apple OS X Server
+administration application; in this mode of operation is reads stdin for
+keys provided in plist format, and writes results to stdout, also in plist
+form.  For interactive use, you can list one or more keys as command line
+arguments.
+.Pp
+.Sh OPTIONS
+.Bl -tag -width flag
+.It Fl h, -help
+Displays usage information
+.It Fl f, -config Ar FILE
+Use the Calendar Server configuration specified in the given file.
+Defaults to /Applications/Server.app/Contents/ServerRoot/private/etc/caldavd/caldavd-apple.plist on Apple servers, /etc/caldavd/caldavd.plist on other servers.
+.El
+.Sh EXAMPLES
+Retrieve the value for EnableCalDAV
+.Pp
+.Dl "calendarserver_config EnableCalDAV"
+.Pp
+Set EnableCalDAV to True
+.Pp
+.Dl "calendarserver_config EnableCalDAV=True"
+.Pp
+.Sh FILES
+.Bl -tag -width flag
+.It /Applications/Server.app/Contents/ServerRoot/private/etc/caldavd/caldavd-apple.plist
+The static Calendar Server configuration file (not to be edited).
+.It /Library/Server/Calendar and Contacts/Config/caldavd-system.plist
+The configuration file which stores local overriding values (which calendarserver_config modifies).
+.El

Modified: CalendarServer/branches/users/sagen/testing/run
===================================================================
--- CalendarServer/branches/users/sagen/testing/run	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/run	2013-03-15 17:01:43 UTC (rev 10930)
@@ -190,20 +190,15 @@
 
   # If we've been asked to read a configuration key, just read it and exit.
   if [ -n "${read_key}" ]; then
-    "${caldav}/bin/calendarserver_config" "${read_key}";
+    value="$("${caldav}/bin/calendarserver_config" "${read_key}")";
+    IFS="="; set ${value}; echo "$2"; unset IFS;
     exit $?;
   fi;
 
   if "${kill}" || "${restart}"; then
-    # mimic logic of 'fullServerPath' from twistedcaldav/config.py to find the pid file
     pidfile="$("${caldav}/bin/calendarserver_config" "PIDFile")";
-    serverroot="$("${caldav}/bin/calendarserver_config" "ServerRoot")";
-    runroot="$("${caldav}/bin/calendarserver_config" "RunRoot")";
-    # examine first character of $pidfile
-    if ( [ "${pidfile:0:1}" == "/" ] || [ "${pidfile:0:1}" == "." ]; ) then
-        pidfile=$pidfile;
-        else pidfile=${serverroot}/${runroot}/${pidfile};
-    fi
+    # Split key and value on "=" and just grab the value
+    IFS="="; set ${pidfile}; pidfile="$2"; unset IFS;
     if [ ! -r "${pidfile}" ]; then
       echo "Unreadable PID file: ${pidfile}";
       exit 1

Modified: CalendarServer/branches/users/sagen/testing/setup.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/setup.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/setup.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -132,7 +132,7 @@
                              "bin/calendarserver_backup",
                              "bin/calendarserver_bootstrap_database",
                              "bin/calendarserver_command_gateway",
-                            #"bin/calendarserver_config", # Used by run script.
+                             "bin/calendarserver_config",
                             #"bin/calendarserver_dbinspect",
                             #"bin/calendarserver_dkimtool",
                              "bin/calendarserver_export",

Modified: CalendarServer/branches/users/sagen/testing/support/Makefile.Apple
===================================================================
--- CalendarServer/branches/users/sagen/testing/support/Makefile.Apple	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/support/Makefile.Apple	2013-03-15 17:01:43 UTC (rev 10930)
@@ -85,7 +85,7 @@
 	$(_v) cd $(BuildDirectory)/pycrypto-2.5       && $(Environment) $(PYTHON) setup.py install $(PY_INSTALL_FLAGS)
 	$(_v) for so in $$(find "$(DSTROOT)$(PY_HOME)/lib" -type f -name '*.so'); do $(STRIP) -Sx "$${so}"; done 
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)"
-	$(_v) $(INSTALL_FILE) "$(Sources)/conf/caldavd-apple.plist" "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)/caldavd.plist"
+	$(_v) $(INSTALL_FILE) "$(Sources)/conf/caldavd-apple.plist" "$(DSTROOT)$(SIPP)$(ETCDIR)$(CALDAVDSUBDIR)/caldavd-apple.plist"
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(ETCDIR)$(WEBAPPSSUBDIR)"
 	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/com.apple.webapp.contacts.plist" "$(DSTROOT)$(SIPP)$(ETCDIR)$(WEBAPPSSUBDIR)/com.apple.webapp.contacts.plist"
 	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/com.apple.webapp.contactsssl.plist" "$(DSTROOT)$(SIPP)$(ETCDIR)$(WEBAPPSSUBDIR)/com.apple.webapp.contactsssl.plist"
@@ -112,22 +112,6 @@
 	$(_v) $(INSTALL_DIRECTORY) -o "$(CS_USER)" -g "$(CS_GROUP)" -m 0755 "$(DSTROOT)$(VARDIR)/log$(CALDAVDSUBDIR)"
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(NSLIBRARYDIR)/LaunchDaemons"
 	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/launchd/calendarserver.plist" "$(DSTROOT)$(SIPP)$(NSLIBRARYDIR)/LaunchDaemons/org.calendarserver.calendarserver.plist"
-	@echo "Installing migration extras script..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SERVERSETUP)/MigrationExtras"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/calendarmigrator.py" "$(DSTROOT)$(SERVERSETUP)/MigrationExtras/70_calendarmigrator.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SERVERSETUP)/MigrationExtras/70_calendarmigrator.py"
-	@echo "Installing common extras script..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SERVERSETUP)/CommonExtras"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/calendarcommonextra.py" "$(DSTROOT)$(SERVERSETUP)/CommonExtras/70_calendarcommonextra.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SERVERSETUP)/CommonExtras/70_calendarcommonextra.py"
-	@echo "Installing server promotion extras script..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SERVERSETUP)/PromotionExtras"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/calendarpromotion.py" "$(DSTROOT)$(SERVERSETUP)/PromotionExtras/59_calendarpromotion.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SERVERSETUP)/PromotionExtras/59_calendarpromotion.py"
-	@echo "Installing server uninstall extras script..."
-	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SERVERSETUP)/UninstallExtras"
-	$(_v) $(INSTALL_FILE) "$(Sources)/contrib/migration/calendardemotion.py" "$(DSTROOT)$(SERVERSETUP)/UninstallExtras/59_calendardemotion.py"
-	$(_v) chmod ugo+x "$(DSTROOT)$(SERVERSETUP)/UninstallExtras/59_calendardemotion.py"
 	@echo "Installing changeip script..."
 	$(_v) $(INSTALL_DIRECTORY) "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/changeip"
 	$(_v) $(INSTALL_FILE) "$(Sources)/calendarserver/tools/changeip_calendar.py" "$(DSTROOT)$(SIPP)$(LIBEXECDIR)/changeip/changeip_calendar.py"

Modified: CalendarServer/branches/users/sagen/testing/twext/enterprise/queue.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/twext/enterprise/queue.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/twext/enterprise/queue.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -1197,8 +1197,6 @@
         return self.choosePerformer(onlyLocally=True).performWork(table, workID)
 
 
-
-
     def allWorkItemTypes(self):
         """
         Load all the L{WorkItem} types that this node can process and return
@@ -1208,7 +1206,14 @@
         """
         # TODO: For completeness, this may need to involve a plugin query to
         # make sure that all WorkItem subclasses are imported first.
-        return WorkItem.__subclasses__()
+        for workItemSubclass in WorkItem.__subclasses__():
+            # TODO: It might be a good idea to offload this table-filtering to
+            # SchemaSyntax.__contains__, adding in some more structure-
+            # comparison of similarly-named tables.  For now a name check is
+            # sufficient.
+            if workItemSubclass.table.model.name in set([x.model.name for x in
+                                                         self.schema]):
+                yield workItemSubclass
 
 
     def totalNumberOfNodes(self):

Modified: CalendarServer/branches/users/sagen/testing/twext/enterprise/test/test_queue.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/twext/enterprise/test/test_queue.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/twext/enterprise/test/test_queue.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -337,7 +337,7 @@
 
 
     @inlineCallbacks
-    def FIXME_test_notBeforeWhenCheckingForLostWork(self):
+    def test_notBeforeWhenCheckingForLostWork(self):
         """
         L{PeerConnectionPool._periodicLostWorkCheck} should execute any
         outstanding work items, but only those that are expired.

Modified: CalendarServer/branches/users/sagen/testing/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/twistedcaldav/config.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/twistedcaldav/config.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -33,7 +33,7 @@
 class ConfigDict(dict):
     """
     Dictionary which can be accessed using attribute syntax, because
-    that reads an writes nicer in code.  For example:
+    that reads and writes nicer in code.  For example:
       C{config.Thingo.Tiny.Tweak}
     instead of:
       C{config["Thingo"]["Tiny"]["Tweak"]}
@@ -211,7 +211,7 @@
         self.reset()
 
     def updateDefaults(self, items):
-        _mergeData(self._provider.getDefaults(), items)
+        mergeData(self._provider.getDefaults(), items)
         self.update(items)
 
     def update(self, items=None, reloading=False):
@@ -225,7 +225,7 @@
         # Call hooks
         for hook in self._preUpdateHooks:
             hook(self._data, items, reloading=reloading)
-        _mergeData(self._data, items)
+        mergeData(self._data, items)
         for hook in self._postUpdateHooks:
             hook(self._data, reloading=reloading)
 
@@ -234,7 +234,7 @@
 
     def load(self, configFile):
         self._provider.setConfigFileName(configFile)
-        configDict = ConfigDict(self._provider.loadConfig())
+        configDict = self._provider.loadConfig()
         if not self._provider.hasErrors():
             self.update(configDict)
         else:
@@ -242,7 +242,7 @@
                                      % (self._provider.getConfigFileName(),))
 
     def reload(self):
-        configDict = ConfigDict(self._provider.loadConfig())
+        configDict = self._provider.loadConfig()
         if not self._provider.hasErrors():
             if self._beforeResetHook:
                 # Give the beforeResetHook a chance to stash away values we want
@@ -263,7 +263,15 @@
         self._data = ConfigDict(copy.deepcopy(self._provider.getDefaults()))
         self._dirty = True
 
-def _mergeData(oldData, newData):
+def mergeData(oldData, newData):
+    """
+    Merge two ConfigDict objects; oldData will be updated with all the keys
+    and values from newData
+    @param oldData: the object to modify
+    @type oldData: ConfigDict
+    @param newData: the object to copy data from
+    @type newData: ConfigDict
+    """
     for key, value in newData.iteritems():
         if isinstance(value, (dict,)):
             if key in oldData:
@@ -271,7 +279,7 @@
                     "%r in %r is not a ConfigDict" % (oldData[key], oldData)
             else:
                 oldData[key] = {}
-            _mergeData(oldData[key], value)
+            mergeData(oldData[key], value)
         else:
             oldData[key] = value
 

Modified: CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/appleopendirectory.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/appleopendirectory.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -56,17 +56,22 @@
         return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
 
 
-    def __init__(self, params):
+    def __init__(self, params, odModule=None):
         """
         @param params: a dictionary containing the following keys:
-            node: an OpenDirectory node name to bind to.
-            restrictEnabledRecords: C{True} if a group in the
-              directory is to be used to determine which calendar
-              users are enabled.
-            restrictToGroup: C{str} guid or name of group used to
-              restrict enabled users.
-            cacheTimeout: C{int} number of minutes before cache is invalidated.
-            negativeCache: C{False} cache the fact that a record wasn't found
+
+            - node: an OpenDirectory node name to bind to.
+
+            - restrictEnabledRecords: C{True} if a group in the directory is to
+              be used to determine which calendar users are enabled.
+
+            - restrictToGroup: C{str} guid or name of group used to restrict
+              enabled users.
+
+            - cacheTimeout: C{int} number of minutes before cache is
+              invalidated.
+
+            - negativeCache: C{False} cache the fact that a record wasn't found
         """
         defaults = {
             'node' : '/Search',
@@ -90,7 +95,9 @@
         super(OpenDirectoryService, self).__init__(params['cacheTimeout'],
                                                    params['negativeCaching'])
 
-        self.odModule = namedModule(config.OpenDirectoryModule)
+        if odModule is None:
+            odModule = namedModule(config.OpenDirectoryModule)
+        self.odModule = odModule
 
         try:
             directory = self.odModule.odInit(params['node'])
@@ -1447,7 +1454,7 @@
                     self.shortNames[0],
                     challenge,
                     response,
-                    credentials.originalMethod if credentials.originalMethod else credentials.method
+                    credentials.method
                 ):
                     try:
                         cache = self.digestcache
@@ -1465,7 +1472,8 @@
     Challenge: %s
     Response:  %s
     Method:    %s
-""" % (self.nodeName, self.shortNames[0], challenge, response, credentials.originalMethod if credentials.originalMethod else credentials.method))
+""" % (self.nodeName, self.shortNames[0], challenge, response,
+       credentials.method))
 
             except self.service.odModule.ODError, e:
                 self.log_error(

Modified: CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/directory.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/directory.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -49,7 +49,7 @@
 from twext.python.log import Logger, LoggingMixIn
 
 from twistedcaldav.config import config
-# from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 
 from twistedcaldav.directory.idirectory import IDirectoryService, IDirectoryRecord
 from twistedcaldav.directory.util import uuidFromName, normalizeUUID

Modified: CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/test/test_opendirectory.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/twistedcaldav/directory/test/test_opendirectory.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -19,6 +19,7 @@
 except ImportError:
     pass
 else:
+    from collections import defaultdict
     from twisted.trial.unittest import SkipTest
     from twisted.internet.defer import inlineCallbacks
     from twisted.python.runtime import platform
@@ -28,7 +29,24 @@
     from twistedcaldav.directory.directory import DirectoryService
     from twistedcaldav.directory.appleopendirectory import OpenDirectoryRecord
     from calendarserver.platform.darwin.od import dsattributes
+    from txdav.common.datastore.test.util import deriveValue, withSpecialValue
 
+    class DigestAuthModule(object):
+        """
+        Stand-in for either configurable OD module, that verifies the response
+        according to its '.response' attribute, set by the test.
+        """
+        class ODError(Exception):
+            pass
+
+        def odInit(self, node):
+            return self
+
+        def authenticateUserDigest(self, directory, node, user, challenge,
+                                   response, method):
+            val = (response == self.response)
+            return val
+
     # Wonky hack to prevent unclean reactor shutdowns
     class DummyReactor(object):
         @staticmethod
@@ -60,7 +78,8 @@
                     {
                         "node" : "/Search",
                         "augmentService": augment.AugmentXMLDB(xmlFiles=()),
-                    }
+                    },
+                    odModule=deriveValue(self, "odModule", lambda self: None)
                 )
             except ImportError, e:
                 raise SkipTest("OpenDirectory module is not available: %s" % (e,))
@@ -87,6 +106,8 @@
             )
             self.assertEquals(record.fullName, "")
 
+
+        @withSpecialValue("odModule", DigestAuthModule())
         def test_invalidODDigest(self):
             record = OpenDirectoryRecord(
                 service               = self.service(),
@@ -105,11 +126,16 @@
                 extReadOnlyProxies    = [],
             )
 
-            digestFields = {}
-            digested = DigestedCredentials("user", "GET", "example.com", digestFields, None)
+            digestFields = defaultdict(lambda: "...")
+            digested = DigestedCredentials("user", "GET", "example.com",
+                                           digestFields)
+            od = deriveValue(self, "odModule", lambda x: None)
+            od.response = "invalid"
 
             self.assertFalse(record.verifyCredentials(digested))
 
+
+        @withSpecialValue("odModule", DigestAuthModule())
         def test_validODDigest(self):
             record = OpenDirectoryRecord(
                 service               = self.service(),
@@ -136,8 +162,8 @@
                 "response":"123",
                 "algorithm":"md5",
             }
-
-            response = (
+            od = deriveValue(self, "odModule", lambda self: None)
+            od.response = (
                 'Digest username="%(username)s", '
                 'realm="%(realm)s", '
                 'nonce="%(nonce)s", '
@@ -146,9 +172,8 @@
                 'algorithm=%(algorithm)s'
             ) % digestFields
 
-            record.digestcache = {}
-            record.digestcache["/"] = response
-            digested = DigestedCredentials("user", "GET", "example.com", digestFields, None)
+            digested = DigestedCredentials("user", "GET", "example.com",
+                                           digestFields)
 
             self.assertTrue(record.verifyCredentials(digested))
 
@@ -469,5 +494,6 @@
                     "node" : "/Search",
                     "recordTypes" : (DirectoryService.recordType_users, DirectoryService.recordType_groups),
                     "augmentService" : augment.AugmentXMLDB(xmlFiles=()),
-                }
+                },
+                odModule=deriveValue(self, "odModule", lambda x: None)
             )

Modified: CalendarServer/branches/users/sagen/testing/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/twistedcaldav/stdconfig.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/twistedcaldav/stdconfig.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -29,8 +29,8 @@
 from twext.python.log import clearLogLevels, setLogLevelForNamespace
 
 from twistedcaldav import caldavxml, customxml, carddavxml, mkcolxml
-from twistedcaldav.config import ConfigProvider, ConfigurationError
-from twistedcaldav.config import config, _mergeData, fullServerPath
+from twistedcaldav.config import ConfigProvider, ConfigurationError, ConfigDict
+from twistedcaldav.config import config, mergeData, fullServerPath
 from twistedcaldav.util import getPasswordFromKeychain
 from twistedcaldav.util import KeychainAccessError, KeychainPasswordNotFound
 
@@ -41,7 +41,7 @@
 log = Logger()
 
 if platform.isMacOSX():
-    DEFAULT_CONFIG_FILE = "/Library/Server/Calendar and Contacts/Config/caldavd.plist"
+    DEFAULT_CONFIG_FILE = "/Applications/Server.app/Contents/ServerRoot/private/etc/caldavd/caldavd-apple.plist"
 else:
     DEFAULT_CONFIG_FILE = "/etc/caldavd/caldavd.plist"
 
@@ -153,6 +153,7 @@
         },
         "resourceSchema": {
             "resourceInfoAttr": None, # contains location/resource info
+            "autoAcceptGroupAttr": None, # auto accept group
         },
         "partitionSchema": {
             "serverIdAttr": None, # maps to augments server-id
@@ -973,7 +974,10 @@
     # America/Los_Angeles.
     "DefaultTimezone" : "",
 
+    # These two aren't relative to ConfigRoot:
     "Includes": [], # Other plists to parse after this one
+    "WritableConfigFile" : "", # which config file calendarserver_config should
+        # write to for changes; empty string means the main config file.
 }
 
 
@@ -1001,15 +1005,19 @@
         configDict = {}
         if self._configFileName:
             configDict = self._parseConfigFromFile(self._configFileName)
+        configDict = ConfigDict(configDict)
         # Now check for Includes and parse and add each of those
         if "Includes" in configDict:
-            configRoot = os.path.join(configDict.ServerRoot, configDict.ConfigRoot)
             for include in configDict.Includes:
-                path = _expandPath(fullServerPath(configRoot, include))
-                additionalDict = self._parseConfigFromFile(path)
-                if additionalDict:
-                    log.info("Adding configuration from file: '%s'" % (path,))
-                    configDict.update(additionalDict)
+                # Includes are not relative to ConfigRoot
+                path = _expandPath(include)
+                if os.path.exists(path):
+                    additionalDict = ConfigDict(self._parseConfigFromFile(path))
+                    if additionalDict:
+                        log.info("Adding configuration from file: '%s'" % (path,))
+                        mergeData(configDict, additionalDict)
+                else:
+                    log.warn("Missing configuration file: '%s'" % (path,))
         return configDict
 
 
@@ -1026,7 +1034,6 @@
         return configDict
 
 
-
 def _expandPath(path):
     if '$' in path:
         return path.replace('$', getfqdn())
@@ -1065,7 +1072,6 @@
     Post-update configuration hook for making all configured paths relative to
     their respective root directories rather than the current working directory.
     """
-
     # Remove possible trailing slash from ServerRoot
     try:
         configDict["ServerRoot"] = configDict["ServerRoot"].rstrip("/")
@@ -1128,7 +1134,7 @@
         if dsType == configDict.DirectoryService.type:
             oldParams = configDict.DirectoryService.params
             newParams = items.DirectoryService.get("params", {})
-            _mergeData(oldParams, newParams)
+            mergeData(oldParams, newParams)
         else:
             if dsType in DEFAULT_SERVICE_PARAMS:
                 configDict.DirectoryService.params = copy.deepcopy(DEFAULT_SERVICE_PARAMS[dsType])
@@ -1158,7 +1164,7 @@
         if dsType == configDict.ResourceService.type:
             oldParams = configDict.ResourceService.params
             newParams = items.ResourceService.get("params", {})
-            _mergeData(oldParams, newParams)
+            mergeData(oldParams, newParams)
         else:
             if dsType in DEFAULT_RESOURCE_PARAMS:
                 configDict.ResourceService.params = copy.deepcopy(DEFAULT_RESOURCE_PARAMS[dsType])
@@ -1190,7 +1196,7 @@
         if dsType == configDict.DirectoryAddressBook.type:
             oldParams = configDict.DirectoryAddressBook.params
             newParams = items["DirectoryAddressBook"].get("params", {})
-            _mergeData(oldParams, newParams)
+            mergeData(oldParams, newParams)
         else:
             if dsType in directoryAddressBookBackingServiceDefaultParams:
                 configDict.DirectoryAddressBook.params = copy.deepcopy(directoryAddressBookBackingServiceDefaultParams[dsType])
@@ -1201,7 +1207,7 @@
         if param not in directoryAddressBookBackingServiceDefaultParams[dsType]:
             raise ConfigurationError("Parameter %s is not supported by service %s" % (param, dsType))
 
-    _mergeData(configDict, items)
+    mergeData(configDict, items)
 
     for param in tuple(configDict.DirectoryAddressBook.params):
         if param not in directoryAddressBookBackingServiceDefaultParams[configDict.DirectoryAddressBook.type]:
@@ -1364,10 +1370,20 @@
             ):
                 if not service[protocol]["Topic"]:
                     certPath = service[protocol]["CertificatePath"]
-                    if certPath and os.path.exists(certPath):
-                        topic = getAPNTopicFromCertificate(certPath)
-                        service[protocol]["Topic"] = topic
+                    if certPath:
+                        if os.path.exists(certPath):
+                            topic = getAPNTopicFromCertificate(certPath)
+                            service[protocol]["Topic"] = topic
+                        else:
+                            log.error("APNS certificate not found: %s" %
+                                (certPath,))
+                    else:
+                        log.error("APNS certificate path not specified")
 
+                if not service[protocol]["Topic"]:
+                    log.error("APNS cannot proceed; disabling APNS")
+                    service["Enabled"] = False
+
                 # If we already have the cert passphrase, don't fetch it again
                 if service[protocol]["Passphrase"]:
                     continue
@@ -1377,13 +1393,13 @@
                 try:
                     passphrase = getPasswordFromKeychain(accountName)
                     service[protocol]["Passphrase"] = passphrase
-                    log.info("%s APN certificate passphrase retreived from keychain" % (protocol,))
+                    log.info("%s APNS certificate passphrase retreived from keychain" % (protocol,))
                 except KeychainAccessError:
                     # The system doesn't support keychain
                     pass
                 except KeychainPasswordNotFound:
                     # The password doesn't exist in the keychain.
-                    log.info("%s APN certificate passphrase not found in keychain" % (protocol,))
+                    log.info("%s APNS certificate passphrase not found in keychain" % (protocol,))
 
 
 

Modified: CalendarServer/branches/users/sagen/testing/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/twistedcaldav/test/test_config.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/twistedcaldav/test/test_config.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -17,7 +17,7 @@
 from twext.python.plistlib import writePlist #@UnresolvedImport
 from twext.python.log import logLevelForNamespace
 
-from twistedcaldav.config import config, ConfigDict
+from twistedcaldav.config import config, ConfigDict, mergeData
 from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, PListConfigProvider,\
     RELATIVE_PATHS
@@ -371,6 +371,40 @@
         configDict._x = "X"
         self.assertEquals(configDict._x, "X")
 
+    def test_mergeData(self):
+        """
+        Verify we don't lose keys which are present in the old but not
+        replaced in the new.
+        """
+        old = ConfigDict({
+            "Scheduling" : ConfigDict({
+                "iMIP" : ConfigDict({
+                    "Enabled" : True,
+                    "Receiving" : ConfigDict({
+                        "Username" : "xyzzy",
+                        "Server" : "example.com",
+                    }),
+                    "Sending" : ConfigDict({
+                        "Username" : "plugh",
+                    }),
+                    "AddressPatterns" : ["mailto:.*"],
+                }),
+            }),
+        })
+        new = ConfigDict({
+            "Scheduling" : ConfigDict({
+                "iMIP" : ConfigDict({
+                    "Enabled" : False,
+                    "Receiving" : ConfigDict({
+                        "Username" : "changed",
+                    }),
+                }),
+            }),
+        })
+        mergeData(old, new)
+        self.assertEquals(old.Scheduling.iMIP.Receiving.Server, "example.com")
+        self.assertEquals(old.Scheduling.iMIP.Sending.Username, "plugh")
+
     def test_SimpleInclude(self):
 
         testConfigMaster = """<?xml version="1.0" encoding="UTF-8"?>

Modified: CalendarServer/branches/users/sagen/testing/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/txdav/caldav/datastore/sql.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/txdav/caldav/datastore/sql.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -1162,7 +1162,7 @@
             # Determine attachment mode (ignore inbox's) - NB we have to do this
             # after setting up other properties as UID at least is needed
             self._attachment = _ATTACHMENTS_MODE_NONE
-            if self._dropboxID is None:
+            if not self._dropboxID:
                 if self._parentCollection.name() != "inbox":
                     if component.hasPropertyInAnyComponent("X-APPLE-DROPBOX"):
                         self._attachment = _ATTACHMENTS_MODE_WRITE
@@ -1582,7 +1582,7 @@
         for managed_id in added:
             changed[managed_id] = newattached[managed_id]
 
-        if self._dropboxID is None:
+        if not self._dropboxID:
             self._dropboxID = str(uuid.uuid4())
         changes = yield self._addingManagedIDs(self._txn, self._parentCollection, self._dropboxID, changed, component.resourceUID())
 
@@ -1714,7 +1714,7 @@
             raise AttachmentStoreFailed
         yield t.loseConnection()
 
-        if self._dropboxID is None:
+        if not self._dropboxID:
             self._dropboxID = str(uuid.uuid4())
         attachment._objectDropboxID = self._dropboxID
 
@@ -2119,7 +2119,7 @@
         @return: C{True} if this attachment exists, C{False} otherwise.
         """
         att = schema.ATTACHMENT
-        if self._dropboxID is not None:
+        if self._dropboxID:
             where = (att.DROPBOX_ID == self._dropboxID).And(
                    att.PATH == self._name)
         else:

Modified: CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql.py	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql.py	2013-03-15 17:01:43 UTC (rev 10930)
@@ -353,7 +353,7 @@
             toFile.write("\n")
             toFile.write("SQL: %s\n" % (sql,))
             toFile.write("Rows: %s\n" % (rows,))
-            toFile.write("Time (ms): %.3f\n" % (t,))
+            toFile.write("Time (ms): %.3f\n" % (t * 1000.0,))
         toFile.write("***\n\n")
 
         if self.logFileName:

Modified: CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql
===================================================================
--- CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_12_to_13.sql	2013-03-15 17:01:43 UTC (rev 10930)
@@ -22,13 +22,24 @@
 
 create sequence ATTACHMENT_ID_SEQ;
 
+alter table ATTACHMENT
+ drop primary key;
 
+-- not needed; DROPBOX_ID becomes nullable after dropping primary key above
+-- alter table ATTACHMENT
+--  modify (DROPBOX_ID null);
+
+-- We want ATTACHMENT_ID as a pkey, but can't yet since it needs unique values,
+-- and oracle can't set a default column value from a sequence
 alter table ATTACHMENT
- drop primary key ("DROPBOX_ID", "PATH");
+ add ("ATTACHMENT_ID" integer);
+
+-- fill in ATTACHMENT_ID with unique values from the sequence
+update ATTACHMENT set ATTACHMENT_ID = ATTACHMENT_ID_SEQ.nextval;
+
+-- now set ATTACHMENT_ID as primary key, which implies unique and not null
 alter table ATTACHMENT
- modify (DROPBOX_ID null);
-alter table ATTACHMENT
- add ("ATTACHMENT_ID" integer primary key);
+ add primary key(ATTACHMENT_ID);
 
 create table ATTACHMENT_CALENDAR_OBJECT (
     "ATTACHMENT_ID" integer not null references ATTACHMENT on delete cascade,

Modified: CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql
===================================================================
--- CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql	2013-03-15 12:28:14 UTC (rev 10929)
+++ CalendarServer/branches/users/sagen/testing/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_13_to_14.sql	2013-03-15 17:01:43 UTC (rev 10930)
@@ -27,7 +27,7 @@
 alter table CALENDAR_BIND
  drop column SEEN_BY_SHAREE;
 alter table CALENDAR_BIND
- modify (ADDRESSBOOK_RESOURCE_NAME not null);
+ modify (CALENDAR_RESOURCE_NAME not null);
  
 alter table ADDRESSBOOK_BIND
  drop column SEEN_BY_OWNER;
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130315/d69896fd/attachment-0001.html>


More information about the calendarserver-changes mailing list