[CalendarServer-changes] [10924] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Mar 14 14:14:13 PDT 2013


Revision: 10924
          http://trac.calendarserver.org//changeset/10924
Author:   sagen at apple.com
Date:     2013-03-14 14:14:13 -0700 (Thu, 14 Mar 2013)
Log Message:
-----------
Config files can be split up between a static plist and a separate plist containing local changes, and the former includes the latter.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tools/config.py
    CalendarServer/trunk/calendarserver/tools/gateway.py
    CalendarServer/trunk/calendarserver/tools/test/gateway/caldavd.plist
    CalendarServer/trunk/calendarserver/tools/test/test_gateway.py
    CalendarServer/trunk/conf/caldavd-apple.plist
    CalendarServer/trunk/setup.py
    CalendarServer/trunk/support/Makefile.Apple
    CalendarServer/trunk/twistedcaldav/config.py
    CalendarServer/trunk/twistedcaldav/scheduling/imip/inbound.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/test/test_config.py

Added Paths:
-----------
    CalendarServer/trunk/calendarserver/tools/test/test_config.py
    CalendarServer/trunk/doc/calendarserver_config.8

Removed Paths:
-------------
    CalendarServer/trunk/contrib/migration/__init__.py
    CalendarServer/trunk/contrib/migration/calendarcommonextra.py
    CalendarServer/trunk/contrib/migration/calendardemotion.py
    CalendarServer/trunk/contrib/migration/calendarmigrator.py
    CalendarServer/trunk/contrib/migration/calendarpromotion.py
    CalendarServer/trunk/contrib/migration/test/

Modified: CalendarServer/trunk/calendarserver/tools/config.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/config.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/calendarserver/tools/config.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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 twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
+from twext.python.plistlib import readPlistFromString, writePlistToString
+from twistedcaldav.config import config, ConfigDict, ConfigurationError, mergeData
+from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE, DEFAULT_CONFIG
+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/trunk/calendarserver/tools/gateway.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/gateway.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/calendarserver/tools/gateway.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/calendarserver/tools/test/gateway/caldavd.plist
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/gateway/caldavd.plist	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/calendarserver/tools/test/gateway/caldavd.plist	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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>

Added: CalendarServer/trunk/calendarserver/tools/test/test_config.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_config.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/tools/test/test_config.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/calendarserver/tools/test/test_gateway.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_gateway.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/calendarserver/tools/test/test_gateway.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-apple.plist	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/conf/caldavd-apple.plist	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/contrib/migration/__init__.py
===================================================================
--- CalendarServer/trunk/contrib/migration/__init__.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/contrib/migration/__init__.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/contrib/migration/calendarcommonextra.py
===================================================================
--- CalendarServer/trunk/contrib/migration/calendarcommonextra.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/contrib/migration/calendarcommonextra.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/contrib/migration/calendardemotion.py
===================================================================
--- CalendarServer/trunk/contrib/migration/calendardemotion.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/contrib/migration/calendardemotion.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/contrib/migration/calendarmigrator.py
===================================================================
--- CalendarServer/trunk/contrib/migration/calendarmigrator.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/contrib/migration/calendarmigrator.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/contrib/migration/calendarpromotion.py
===================================================================
--- CalendarServer/trunk/contrib/migration/calendarpromotion.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/contrib/migration/calendarpromotion.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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()

Added: CalendarServer/trunk/doc/calendarserver_config.8
===================================================================
--- CalendarServer/trunk/doc/calendarserver_config.8	                        (rev 0)
+++ CalendarServer/trunk/doc/calendarserver_config.8	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/setup.py
===================================================================
--- CalendarServer/trunk/setup.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/setup.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/support/Makefile.Apple
===================================================================
--- CalendarServer/trunk/support/Makefile.Apple	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/support/Makefile.Apple	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/twistedcaldav/config.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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/trunk/twistedcaldav/scheduling/imip/inbound.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip/inbound.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip/inbound.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -21,6 +21,7 @@
 from calendarserver.tap.util import FakeRequest
 import email.utils
 from twext.enterprise.dal.record import fromTable
+from twext.enterprise.dal.syntax import Delete
 from twext.enterprise.queue import WorkItem
 from twext.python.log import Logger, LoggingMixIn
 from twisted.application import service
@@ -78,11 +79,15 @@
 
 class IMIPPollingWork(WorkItem, fromTable(schema.IMIP_POLLING_WORK)):
 
-    # FIXME: delete all other polling work items
     # FIXME: purge all old tokens here
+    group = "imip_polling"
 
     @inlineCallbacks
     def doWork(self):
+
+        # Delete all other work items
+        yield Delete(From=self.table, Where=None).on(self.transaction)
+
         mailRetriever = self.transaction._mailRetriever
         if mailRetriever is not None:
             try:

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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"
 
@@ -975,7 +975,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.
 }
 
 
@@ -1003,15 +1006,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
 
 
@@ -1028,7 +1035,6 @@
         return configDict
 
 
-
 def _expandPath(path):
     if '$' in path:
         return path.replace('$', getfqdn())
@@ -1067,7 +1073,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("/")
@@ -1130,7 +1135,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])
@@ -1160,7 +1165,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])
@@ -1192,7 +1197,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])
@@ -1203,7 +1208,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]:

Modified: CalendarServer/trunk/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_config.py	2013-03-14 19:43:14 UTC (rev 10923)
+++ CalendarServer/trunk/twistedcaldav/test/test_config.py	2013-03-14 21:14:13 UTC (rev 10924)
@@ -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"?>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130314/e0b4455d/attachment-0001.html>


More information about the calendarserver-changes mailing list