[CalendarServer-changes] [1139] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Feb 6 14:11:51 PST 2007


Revision: 1139
          http://trac.macosforge.org/projects/calendarserver/changeset/1139
Author:   cdaboo at apple.com
Date:     2007-02-06 14:11:51 -0800 (Tue, 06 Feb 2007)

Log Message:
-----------
Merge branches/users/cdaboo/od-scheme-1044 to trunk.

Modified Paths:
--------------
    CalendarServer/trunk/run
    CalendarServer/trunk/twistedcaldav/config.py
    CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py

Added Paths:
-----------
    CalendarServer/trunk/support/directorysetup.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryschema.py

Modified: CalendarServer/trunk/run
===================================================================
--- CalendarServer/trunk/run	2007-02-06 22:10:09 UTC (rev 1138)
+++ CalendarServer/trunk/run	2007-02-06 22:11:51 UTC (rev 1139)
@@ -417,7 +417,7 @@
   if ! py_have_module opendirectory; then
     opendirectory="${top}/PyOpenDirectory";
 
-    svn_get "PyOpenDirectory" "${opendirectory}" "${svn_uri_base}/PyOpenDirectory/trunk" 1032;
+    svn_get "PyOpenDirectory" "${opendirectory}" "${svn_uri_base}/PyOpenDirectory/trunk" 1138;
     py_build "PyOpenDirectory" "${opendirectory}" false;
     py_install "PyOpenDirectory" "${opendirectory}";
 

Copied: CalendarServer/trunk/support/directorysetup.py (from rev 1136, CalendarServer/branches/users/cdaboo/od-schema-1044/support/directorysetup.py)
===================================================================
--- CalendarServer/trunk/support/directorysetup.py	                        (rev 0)
+++ CalendarServer/trunk/support/directorysetup.py	2007-02-06 22:11:51 UTC (rev 1139)
@@ -0,0 +1,304 @@
+#!/usr/bin/env python
+#
+##
+# Copyright (c) 2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# DRI: Cyrus Daboo, cdaboo at apple.com
+##
+#
+# ...
+#
+
+
+import getopt
+import os
+import sys
+
+def usage():
+    print """Usage: directorysetup [options] init|addUser|disableUser|removeUser <<user>>
+Options:
+    -h       Print this help and exit
+    -n node  OpenDirectory node to target (/LDAPv3/127.0.0.1)
+    -u uid   OpenDirectory Admin user id
+    -p pswd  OpenDirectory Admin user password
+    -c cname OpenDirectory /Computers record name for the calendar server
+    
+Description:
+This is a little command line utility to setup a directory server with records
+conforming to the new schema used by the calendar server. It has several "actions":
+
+"init" - this action will modify the computer record for the host calendar server to
+add the new "com.apple.macosxserver.virtualhosts" entry.
+
+"addUser" - modifies a user record to enable use of the calendar server.
+
+"disableUser" - modifies a user record to disable use of the calendar server.
+
+"removeUser" - modifies a user record to prevent use of the calendar server by
+ removing any reference to the service.
+
+"""
+
+def initComputerRecord(admin_user, admin_pswd, node, recordname):
+    plistdefault = """<?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>ReplicaName</key>
+        <string>Master</string>
+        <key>com.apple.od.role</key>
+        <string>master</string>
+</dict>
+</plist>
+"""
+    plistdefault = plistdefault.replace('"', '\\"')
+    plistdefault = plistdefault.replace('\n', '')
+
+    plist_good = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+
+        <key>com.apple.macosxserver.virtualhosts</key>
+        <dict>
+            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
+            <dict>
+                <key>hostname</key>
+                <string>foo.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>80</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>wiki</string>
+                    <string>webCalendar</string>
+                    <string>webMailingList</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                    <key>wiki</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
+                    </dict>
+                    <key>webMailingList</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
+                    </dict>
+                </dict>
+            </dict>
+            
+            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
+            <dict>
+                <key>hostname</key>
+                <string>calendar.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8008</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>calendar</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>calendar</key>
+                    <dict>
+                        <key>templates</key>
+                        <dict>
+                            <key>principalPath</key>
+                            <string>/principals/%(type)s/%(name)s</string>
+                            <key>calendarUserAddresses</key>
+                            <array>
+                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
+                                <string>mailto:%(email)s</string>
+                                <string>urn:uuid:%(guid)s</string>
+                            </array>
+                        </dict>
+                    </dict>
+                </dict>
+            </dict>
+
+        </dict>
+    </dict>
+</plist>
+"""
+
+    plist_good = plist_good.replace('"', '\\"')
+    plist_good = plist_good.replace('\n', '')
+    cmd = "dscl -u %s -P %s %s -create /Computers/%s \"dsAttrTypeStandard:XMLPlist\" \"%s\"" % (admin_user, admin_pswd, node, recordname, plist_good,)
+    print cmd
+    os.system(cmd)
+
+def getComputerRecordGUID(admin_user, admin_pswd, node, computername):
+    # First get the generatedGUID for this computer
+    cmd = "dscl -u %s -P %s %s -read /Computers/%s \"dsAttrTypeStandard:GeneratedUID\"" % (admin_user, admin_pswd, node, computername,)
+    print cmd
+    result = os.popen(cmd, "r")
+    for line in result:
+        return line[len("GeneratedUID: "):-1]
+
+def addUser(admin_user, admin_pswd, node, computername, username):
+
+    uid = getComputerRecordGUID(admin_user, admin_pswd, node, computername)
+    servicetag = "%s:%s:calendar" % (uid, "C18C34AC-3D9E-403C-8A33-BFC303F3840E")
+
+    cmd = "dscl -u %s -P %s %s -create /Users/%s \"dsAttrTypeStandard:ServicesLocator\" \"%s\"" % (admin_user, admin_pswd, node, username, servicetag,)
+    print cmd
+    os.system(cmd)
+
+def disableUser(admin_user, admin_pswd, node, computername, username):
+
+    uid = getComputerRecordGUID(admin_user, admin_pswd, node, computername)
+    servicetag = "%s:%s:calendar:disabled" % (uid, "C18C34AC-3D9E-403C-8A33-BFC303F3840E")
+
+    cmd = "dscl -u %s -P %s %s -create /Users/%s \"dsAttrTypeStandard:ServicesLocator\" \"%s\"" % (admin_user, admin_pswd, node, username, servicetag,)
+    print cmd
+    os.system(cmd)
+
+def removeUser(admin_user, admin_pswd, node, computername, username):
+    cmd = "dscl -u %s -P %s %s -delete /Users/%s \"dsAttrTypeStandard:ServicesLocator\"" % (admin_user, admin_pswd, node, username,)
+    print cmd
+    os.system(cmd)
+
+if __name__ == "__main__":
+
+    try:
+        options, args = getopt.getopt(sys.argv[1:], "hc:n:p:u:")
+
+        node = "/LDAPv3/127.0.0.1"
+        admin_user = None
+        admin_pswd = None
+        computername = None
+
+        for option, value in options:
+            if option == "-h":
+                usage()
+                sys.exit(0)
+            elif option == "-n":
+                node = value
+            elif option == "-u":
+                admin_user = value
+            elif option == "-p":
+                admin_pswd = value
+            elif option == "-c":
+                computername = value
+            else:
+                print "Unrecognized option: %s" % (option,)
+                usage()
+                raise ValueError
+
+        # Some options are required
+        if not admin_user:
+            print "A user name must be specified with the -u option"
+        if not admin_pswd:
+            print "A password must be specified with the -p option"
+        if not computername:
+            print "A computer record name must be specified with the -c option"
+        if not admin_user or not admin_pswd or not computername:
+            usage()
+            raise ValueError
+            
+        # Process arguments
+        if len(args) == 0:
+            print "No arguments given. One of 'init', 'addUser', 'disableUser', 'removeUser' must be present."
+            usage()
+            raise ValueError
+        elif args[0] not in ("init", "addUser", "disableUser", "removeUser"):
+            print "Wrong arguments given: %s" % (args[0],)
+            usage()
+            raise ValueError
+        
+        if args[0] == "init":
+            if len(args) > 1:
+                print "Too many arguments given to 'init'"
+                usage()
+                raise ValueError
+            initComputerRecord(admin_user, admin_pswd, node, "caldav.apple.com$")
+        elif args[0] == "addUser":
+            if len(args) > 2:
+                print "Too many arguments given to 'addUser'"
+                usage()
+                raise ValueError
+            elif len(args) != 2:
+                print "'addUser' must have one argument - the user name"
+                usage()
+                raise ValueError
+            addUser(admin_user, admin_pswd, node, computername, args[1])
+        elif args[0] == "disableUser":
+            if len(args) > 2:
+                print "Too many arguments given to 'disableUser'"
+                usage()
+                raise ValueError
+            elif len(args) != 2:
+                print "'disableUser' must have one argument - the user name"
+                usage()
+                raise ValueError
+            disableUser(admin_user, admin_pswd, node, computername, args[1])
+        elif args[0] == "removeUser":
+            if len(args) > 2:
+                print "Too many arguments given to 'removeUser'"
+                usage()
+                raise ValueError
+            elif len(args) != 2:
+                print "'removeUser' must have one argument - the user name"
+                usage()
+                raise ValueError
+            removeUser(admin_user, admin_pswd, node, computername, args[1])
+
+    except Exception, e:
+        sys.exit(str(e))


Property changes on: CalendarServer/trunk/support/directorysetup.py
___________________________________________________________________
Name: svn:executable
   + *

Modified: CalendarServer/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py	2007-02-06 22:10:09 UTC (rev 1138)
+++ CalendarServer/trunk/twistedcaldav/config.py	2007-02-06 22:11:51 UTC (rev 1139)
@@ -48,6 +48,7 @@
     'ServerStatsFile': '/Library/CalendarServer/Documents/stats.plist',
     'UserQuotaBytes': 104857600,
     'Verbose': False,
+    'ServerHostName': 'localhost',
     'SACLEnable': False,
     'Authentication': {
         'Basic': {

Modified: CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2007-02-06 22:10:09 UTC (rev 1138)
+++ CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2007-02-06 22:11:51 UTC (rev 1139)
@@ -35,9 +35,12 @@
 from twisted.internet.reactor import callLater
 from twisted.cred.credentials import UsernamePassword
 
+from twistedcaldav.config import config
 from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
 
+from plistlib import readPlistFromString
+
 recordListCacheTimeout = 60 * 30 # 30 minutes
 
 class OpenDirectoryService(DirectoryService):
@@ -49,23 +52,36 @@
     def __repr__(self):
         return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
 
-    def __init__(self, node="/Search"):
+    def __init__(self, node="/Search", dosetup=True):
         """
         @param node: an OpenDirectory node name to bind to.
+        @param dosetup: if C{True} then the directory records are initialized,
+                        if C{False} they are not.
+                        This should only be set to C{False} when doing unit tests.
         """
-        directory = opendirectory.odInit(node)
-        if directory is None:
-            raise OpenDirectoryInitError("Failed to open Open Directory Node: %s" % (node,))
+        try:
+            directory = opendirectory.odInit(node)
+        except opendirectory.ODError, e:
+            log.msg("Open Directory (node=%s) Initialization error: %s" % (node, e))
+            raise
 
         self.realmName = node
         self.directory = directory
         self.node = node
+        self.computerRecordName = ""
         self._records = {}
         self._delayedCalls = set()
 
-        for recordType in self.recordTypes():
-            self.recordsForType(recordType)
+        if dosetup:
+            try:
+                self._lookupVHostRecord()
+            except Exception, e:
+                log.err("Unable to locate virtual host record: %s" % (e,))
+                raise
 
+            for recordType in self.recordTypes():
+                self.recordsForType(recordType)
+
     def __cmp__(self, other):
         if not isinstance(other, DirectoryRecord):
             return super(DirectoryRecord, self).__eq__(other)
@@ -82,6 +98,229 @@
             h = (h + hash(getattr(self, attr))) & sys.maxint
         return h
 
+    def _lookupVHostRecord(self):
+        """
+        Get the OD service record for this host.
+        """
+
+        # The server must have been configured with a virtual hostname.
+        vhostname = config.ServerHostName
+        if not vhostname:
+            raise OpenDirectoryInitError(
+                "There is no virtual hostname configured for the server for use with Open Directory (node=%s)"
+                % (self.realmName,)
+            )
+         
+        # Find a record in /Computers with an ENetAddress attribute value equal to the MAC address
+        # and return some useful attributes.
+        attrs = [
+            dsattributes.kDS1AttrGeneratedUID,
+            dsattributes.kDSNAttrRecordName,
+            dsattributes.kDS1AttrXMLPlist,
+        ]
+
+        records = opendirectory.queryRecordsWithAttributes(
+            self.directory,
+            { dsattributes.kDS1AttrXMLPlist: vhostname },
+            dsattributes.eDSContains,
+            True,    # case insentive for hostnames
+            False,
+            dsattributes.kDSStdRecordTypeComputers,
+            attrs
+        )
+        self._parseComputersRecords(records, vhostname)
+
+    def _parseComputersRecords(self, records, vhostname):
+        
+        # Must have some results
+        if len(records) == 0:
+            raise OpenDirectoryInitError(
+                "Open Directory (node=%s) has no /Computers records with a virtual hostname: %s"
+                % (self.realmName, vhostname,)
+            )
+
+        # Now find a single record that actually matches the hostname
+        found = False
+        for recordname, record in records.iteritems():
+            
+            # Must have XMLPlist value
+            plist = record.get(dsattributes.kDS1AttrXMLPlist, None)
+            if not plist:
+                continue
+            
+            if not self._parseXMLPlist(vhostname, recordname, plist, record[dsattributes.kDS1AttrGeneratedUID]):
+                continue
+            elif found:
+                raise OpenDirectoryInitError(
+                    "Open Directory (node=%s) multiple /Computers records found matching virtual hostname: %s"
+                    % (self.realmName, vhostname,)
+                )
+            else:
+                found = True
+                
+        if not found:
+            raise OpenDirectoryInitError(
+                "Open Directory (node=%s) no /Computers records with an enabled and valid calendar service were found matching virtual hostname: %s"
+                % (self.realmName, vhostname,)
+            )
+    
+    def _parseXMLPlist(self, vhostname, recordname, plist, recordguid):
+        # Parse the plist and look for our special entry
+        plist = readPlistFromString(plist)
+        vhosts = plist.get("com.apple.macosxserver.virtualhosts", None)
+        if not vhosts:
+            log.msg(
+                "Open Directory (node=%s) /Computers/%s record does not have a "
+                "com.apple.macosxserver.virtualhosts in its XMLPlist attribute value"
+                % (self.realmName, recordname)
+            )
+            return False
+        
+        # Iterate over each vhost and find one that is a calendar service
+        hostguid = None
+        for key, value in vhosts.iteritems():
+            serviceTypes = value.get("serviceType", None)
+            if serviceTypes:
+                for type in serviceTypes:
+                    if type == "calendar":
+                        hostguid = key
+                        break
+                    
+        if not hostguid:
+            log.msg(
+                "Open Directory (node=%s) /Computers/%s record does not have a "
+                "calendar service in its XMLPlist attribute value"
+                % (self.realmName, recordname)
+            )
+            return False
+            
+        # Get host name
+        hostname = vhosts[hostguid].get("hostname", None)
+        if not hostname:
+            log.msg(
+                "Open Directory (node=%s) /Computers/%s record does not have "
+                "any host name in its XMLPlist attribute value"
+                % (self.realmName, recordname)
+            )
+            return False
+        if hostname != vhostname:
+            log.msg(
+                "Open Directory (node=%s) /Computers/%s record hostname (%s) "
+                "does not match this server (%s)"
+                % (self.realmName, recordname, hostname, vhostname)
+            )
+            return False
+        
+        # Get host details and create host templates
+        hostdetails = vhosts[hostguid].get("hostDetails", None)
+        if not hostdetails:
+            log.msg(
+                "Open Directory (node=%s) /Computers/%s record does not have "
+                "any host details in its XMLPlist attribute value"
+                % (self.realmName, recordname)
+            )
+            return False
+        self.hostvariants = []
+        for key, value in hostdetails.iteritems():
+            self.hostvariants.append((key, hostname, value["port"]))
+        self.hostvariants = tuple(self.hostvariants)
+        
+        # Look at the service data
+        serviceInfos = vhosts[hostguid].get("serviceInfo", None)
+        if not serviceInfos or not serviceInfos.has_key("calendar"):
+            log.msg(
+                "Open Directory (node=%s) /Computers/%s record does not have a "
+                "calendar service in its XMLPlist attribute value"
+                % (self.realmName, recordname)
+            )
+            return False
+        serviceInfo = serviceInfos["calendar"]
+        
+        # Check that this service is enabled
+        enabled = serviceInfo.get("enabled", True)
+        if not enabled:
+            log.msg(
+                "Open Directory (node=%s) /Computers/%s record does not have an "
+                "enabled calendar service in its XMLPlist attribute value"
+                % (self.realmName, recordname)
+            )
+            return False
+        
+        # Get useful templates
+        templates = serviceInfo.get("templates", None)
+        if not templates or not templates.has_key("calendarUserAddresses"):
+            log.msg(
+                "Open Directory (node=%s) /Computers/%s record does not have a "
+                "template for calendar user addresses in its XMLPlist attribute value"
+                % (self.realmName, recordname)
+            )
+            return False
+        
+        self.computerRecordName = recordname
+
+        # Grab the templates we need for calendar user addresses
+        self.cuaddrtemplates = tuple(templates["calendarUserAddresses"])
+        
+        # Create the string we will use to match users with accounts on this server
+        self.servicetag = "%s:%s:calendar" % (recordguid, hostguid)
+        
+        return True
+    
+    def _templateExpandCalendarUserAddresses(self, recordType, recordName, record):
+        """
+        Expand this services calendar user address templates for the specified record.
+        
+        @param recordName: a C{str} containing the record name being operated on.
+        @param record: a C{dict} containing the attributes retrieved from the directory.
+        @return: a C{set} of C{str} for each expanded calendar user address.
+        """
+        
+        # Make a dict of the substitutions we can do for this record. The only record parameters
+        # we substitute are name, guid and email. Note that email is multi-valued so we have to
+        # create a list of dicts for each one of those.
+        name = recordName
+        type = recordType
+        guid = record.get(dsattributes.kDS1AttrGeneratedUID)
+        emails = record.get(dsattributes.kDSNAttrEMailAddress)
+        if emails is not None and isinstance(emails, str):
+            emails = [emails]
+            
+        subslist = []
+        if emails:
+            for email in emails:
+                subslist.append({
+                    "name"  : name,
+                    "type"  : type,
+                    "guid"  : guid,
+                    "email" : email,
+                })
+        else:
+            subslist.append({
+                "name"  : name,
+                "type"  : type,
+                "guid"  : guid,
+            })
+        
+        # Now do substitutions    
+        result = set()
+        for template in self.cuaddrtemplates:
+            for scheme, hostname, port in self.hostvariants:
+                for subs in subslist:
+                    # Add in host substitution values
+                    subs.update({
+                        "scheme"   : scheme,
+                        "hostname" : hostname,
+                        "port"     : port,
+                    })
+                    
+                    # Special check for no email address for this record
+                    if (template.find("%(email)s") != -1) and not emails:
+                        continue
+
+                    result.add(template % subs)
+                
+        return result
+
     def recordTypes(self):
         return (
             DirectoryService.recordType_users,
@@ -100,10 +339,15 @@
         def reloadCache():
             log.msg("Reloading %s record cache" % (recordType,))
 
+            query = {
+                dsattributes.kDSNAttrServicesLocator: self.servicetag,
+            }
+    
             attrs = [
                 dsattributes.kDS1AttrGeneratedUID,
                 dsattributes.kDS1AttrDistinguishedName,
-                dsattributes.kDSNAttrCalendarPrincipalURI,
+                dsattributes.kDSNAttrEMailAddress,
+                dsattributes.kDSNAttrServicesLocator,
             ]
 
             if recordType == DirectoryService.recordType_users:
@@ -116,31 +360,49 @@
             elif recordType == DirectoryService.recordType_resources:
                 listRecordType = dsattributes.kDSStdRecordTypeResources
             else:
-                raise UnknownRecordTypeError("Unknown Open Directory record type: %s" % (recordType,))
+                raise UnknownRecordTypeError("Unknown Open Directory record type: %s"
+                                             % (recordType,))
 
             records = {}
 
             try:
-                results = opendirectory.listAllRecordsWithAttributes(self.directory, listRecordType, attrs)
+                results = opendirectory.queryRecordsWithAttributes(
+                    self.directory,
+                    query,
+                    dsattributes.eDSStartsWith,
+                    False,
+                    False,
+                    listRecordType,
+                    attrs)
             except opendirectory.ODError, ex:
-                log.msg("OpenDirectory error: %s", str(ex))
+                log.msg("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
                 raise
 
             for (key, value) in results.iteritems():
+                # Make sure this user has service enabled.
+                enabled = True
+                service = value.get(dsattributes.kDSNAttrServicesLocator)
+                if isinstance(service, str):
+                    service = [service]
+                for item in service:
+                    if item.startswith(self.servicetag):
+                        if item.endswith(":disabled"):
+                            enabled = False
+                        break
+                if not enabled:
+                    continue
+
+                # Now get useful record info.
                 shortName = key
                 guid = value.get(dsattributes.kDS1AttrGeneratedUID)
                 if not guid:
                     continue
                 realName = value.get(dsattributes.kDS1AttrDistinguishedName)
 
-                cuaddrs = value.get(dsattributes.kDSNAttrCalendarPrincipalURI)
-                cuaddrset = set()
-                if cuaddrs is not None:
-                    if isinstance(cuaddrs, str):
-                        cuaddrset.add(cuaddrs)
-                    else:
-                        cuaddrset.update(cuaddrs)
+                # Get calendar user addresses expanded from service record templates.
+                cuaddrset = self._templateExpandCalendarUserAddresses(recordType, key, value)
 
+                # Special case for groups.
                 if recordType == DirectoryService.recordType_groups:
                     memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
                     if memberGUIDs is None:
@@ -238,7 +500,8 @@
             try:
                 return opendirectory.authenticateUserBasic(self.service.directory, self.shortName, credentials.password)
             except opendirectory.ODError, e:
-                log.err("OpenDirectory error while performing basic authentication for user %s: %r" % (self.shortName, e))
+                log.err("Open Directory (node=%s) error while performing basic authentication for user %s: %r"
+                        % (self.service.realmName, self.shortName, e))
                 return False
 
         return super(OpenDirectoryRecord, self).verifyCredentials(credentials)

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2007-02-06 22:10:09 UTC (rev 1138)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2007-02-06 22:11:51 UTC (rev 1139)
@@ -50,7 +50,7 @@
 
         def setUp(self):
             super(OpenDirectory, self).setUp()
-            self._service = OpenDirectoryService(node="/Search")
+            self._service = OpenDirectoryService(node="/Search", dosetup=False)
 
         def tearDown(self):
             for call in self._service._delayedCalls:

Copied: CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryschema.py (from rev 1136, CalendarServer/branches/users/cdaboo/od-schema-1044/twistedcaldav/directory/test/test_opendirectoryschema.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryschema.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectoryschema.py	2007-02-06 22:11:51 UTC (rev 1139)
@@ -0,0 +1,1018 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# DRI: Wilfredo Sanchez, wsanchez at apple.com
+##
+
+try:
+    from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
+    from twistedcaldav.directory.appleopendirectory import OpenDirectoryInitError
+    import dsattributes
+except ImportError:
+    pass
+else:
+    from twistedcaldav.directory.directory import DirectoryService
+    import twisted.trial.unittest
+
+    class PlistParse (twisted.trial.unittest.TestCase):
+        """
+        Test Open Directory service schema.
+        """
+        
+        plist_nomacosxserver_key = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+    </dict>
+</plist>
+"""
+
+        plist_nocalendarservice = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+
+        <key>com.apple.macosxserver.virtualhosts</key>
+        <dict>
+            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
+            <dict>
+                <key>hostname</key>
+                <string>foo.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>80</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>wiki</string>
+                    <string>webCalendar</string>
+                    <string>webMailingList</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                    <key>wiki</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
+                    </dict>
+                    <key>webMailingList</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
+                    </dict>
+                </dict>
+            </dict>
+
+        </dict>
+    </dict>
+</plist>
+"""
+
+        plist_noserviceinfo = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+
+        <key>com.apple.macosxserver.virtualhosts</key>
+        <dict>
+            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
+            <dict>
+                <key>hostname</key>
+                <string>foo.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>80</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>wiki</string>
+                    <string>webCalendar</string>
+                    <string>webMailingList</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                    <key>wiki</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
+                    </dict>
+                    <key>webMailingList</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
+                    </dict>
+                </dict>
+            </dict>
+            
+            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
+            <dict>
+                <key>hostname</key>
+                <string>calendar.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8008</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>calendar</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                </dict>
+            </dict>
+
+        </dict>
+    </dict>
+</plist>
+"""
+
+        plist_disabledservice = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+
+        <key>com.apple.macosxserver.virtualhosts</key>
+        <dict>
+            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
+            <dict>
+                <key>hostname</key>
+                <string>foo.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>80</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <string>443</string>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>wiki</string>
+                    <string>webCalendar</string>
+                    <string>webMailingList</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                    <key>wiki</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
+                    </dict>
+                    <key>webMailingList</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
+                    </dict>
+                </dict>
+            </dict>
+            
+            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
+            <dict>
+                <key>hostname</key>
+                <string>calendar.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8008</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>calendar</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>calendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <false/>
+                        <key>templates</key>
+                        <dict>
+                            <key>principalPath</key>
+                            <string>/principals/%(type)s/%(name)s</string>
+                            <key>calendarUserAddresses</key>
+                            <array>
+                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
+                                <string>mailto:%(email)s</string>
+                                <string>urn:uuid:%(guid)s</string>
+                            </array>
+                        </dict>
+                    </dict>
+                </dict>
+            </dict>
+
+        </dict>
+    </dict>
+</plist>
+"""
+
+        plist_nohostname = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+
+        <key>com.apple.macosxserver.virtualhosts</key>
+        <dict>
+            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
+            <dict>
+                <key>hostname</key>
+                <string>foo.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>80</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <string>443</string>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>wiki</string>
+                    <string>webCalendar</string>
+                    <string>webMailingList</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                    <key>wiki</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
+                    </dict>
+                    <key>webMailingList</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
+                    </dict>
+                </dict>
+            </dict>
+            
+            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
+            <dict>
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8008</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>calendar</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>calendar</key>
+                    <dict>
+                        <key>templates</key>
+                        <dict>
+                            <key>principalPath</key>
+                            <string>/principals/%(type)s/%(name)s</string>
+                            <key>calendarUserAddresses</key>
+                            <array>
+                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
+                                <string>mailto:%(email)s</string>
+                                <string>urn:uuid:%(guid)s</string>
+                            </array>
+                        </dict>
+                    </dict>
+                </dict>
+            </dict>
+
+        </dict>
+    </dict>
+</plist>
+"""
+
+        plist_nohostdetails = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+
+        <key>com.apple.macosxserver.virtualhosts</key>
+        <dict>
+            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
+            <dict>
+                <key>hostname</key>
+                <string>foo.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>80</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <string>443</string>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>wiki</string>
+                    <string>webCalendar</string>
+                    <string>webMailingList</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                    <key>wiki</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
+                    </dict>
+                    <key>webMailingList</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
+                    </dict>
+                </dict>
+            </dict>
+            
+            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
+            <dict>
+                <key>hostname</key>
+                <string>calendar.apple.com</string>
+
+                <key>serviceType</key>
+                <array>
+                    <string>calendar</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>calendar</key>
+                    <dict>
+                        <key>templates</key>
+                        <dict>
+                            <key>principalPath</key>
+                            <string>/principals/%(type)s/%(name)s</string>
+                            <key>calendarUserAddresses</key>
+                            <array>
+                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
+                                <string>mailto:%(email)s</string>
+                                <string>urn:uuid:%(guid)s</string>
+                            </array>
+                        </dict>
+                    </dict>
+                </dict>
+            </dict>
+
+        </dict>
+    </dict>
+</plist>
+"""
+
+        plist_nocuaddrtemplates = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+
+        <key>com.apple.macosxserver.virtualhosts</key>
+        <dict>
+            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
+            <dict>
+                <key>hostname</key>
+                <string>foo.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>80</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <string>443</string>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>wiki</string>
+                    <string>webCalendar</string>
+                    <string>webMailingList</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                    <key>wiki</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
+                    </dict>
+                    <key>webMailingList</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
+                    </dict>
+                </dict>
+            </dict>
+            
+            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
+            <dict>
+                <key>hostname</key>
+                <string>calendar.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8008</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>calendar</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>calendar</key>
+                    <dict>
+                        <key>templates</key>
+                        <dict>
+                            <key>principalPath</key>
+                            <string>/principals/%(type)s/%(name)s</string>
+                        </dict>
+                    </dict>
+                </dict>
+            </dict>
+
+        </dict>
+    </dict>
+</plist>
+"""
+        plist_good = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+
+        <key>com.apple.macosxserver.virtualhosts</key>
+        <dict>
+            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
+            <dict>
+                <key>hostname</key>
+                <string>foo.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>80</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <string>443</string>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>wiki</string>
+                    <string>webCalendar</string>
+                    <string>webMailingList</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                    <key>wiki</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
+                    </dict>
+                    <key>webMailingList</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
+                    </dict>
+                </dict>
+            </dict>
+            
+            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
+            <dict>
+                <key>hostname</key>
+                <string>calendar.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8008</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>calendar</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>calendar</key>
+                    <dict>
+                        <key>templates</key>
+                        <dict>
+                            <key>principalPath</key>
+                            <string>/principals/%(type)s/%(name)s</string>
+                            <key>calendarUserAddresses</key>
+                            <array>
+                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
+                                <string>mailto:%(email)s</string>
+                                <string>urn:uuid:%(guid)s</string>
+                            </array>
+                        </dict>
+                    </dict>
+                </dict>
+            </dict>
+
+        </dict>
+    </dict>
+</plist>
+"""
+
+        plist_good_other = """<?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>ReplicaName</key>
+        <string>Master</string>
+
+        <key>com.apple.od.role</key>
+        <string>master</string>
+
+        <key>com.apple.macosxserver.virtualhosts</key>
+        <dict>
+            <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
+            <dict>
+                <key>hostname</key>
+                <string>foo.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>80</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <string>443</string>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>wiki</string>
+                    <string>webCalendar</string>
+                    <string>webMailingList</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>webCalendar</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
+                    </dict>
+                    <key>wiki</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
+                    </dict>
+                    <key>webMailingList</key>
+                    <dict>
+                        <key>enabled</key>
+                        <true/>
+                        <key>urlMask</key>
+                        <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
+                    </dict>
+                </dict>
+            </dict>
+            
+            <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
+            <dict>
+                <key>hostname</key>
+                <string>privatecalendar.apple.com</string>
+
+                <key>hostDetails</key>
+                <dict>
+                    <key>http</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8008</integer>
+                    </dict>
+                    <key>https</key>
+                    <dict>
+                        <key>port</key>
+                        <integer>8443</integer>
+                    </dict>
+                </dict>
+
+                <key>serviceType</key>
+                <array>
+                    <string>calendar</string>
+                </array>
+
+                <key>serviceInfo</key>
+                <dict>
+                    <key>calendar</key>
+                    <dict>
+                        <key>templates</key>
+                        <dict>
+                            <key>principalPath</key>
+                            <string>/principals/%(type)s/%(name)s</string>
+                            <key>calendarUserAddresses</key>
+                            <array>
+                                <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
+                                <string>mailto:%(email)s</string>
+                                <string>urn:uuid:%(guid)s</string>
+                            </array>
+                        </dict>
+                    </dict>
+                </dict>
+            </dict>
+
+        </dict>
+    </dict>
+</plist>
+"""
+
+        def test_plist_errors(self):
+            def _doParse(plist, title):
+                service = OpenDirectoryService(node="/Search", dosetup=False)
+                if service._parseXMLPlist("calendar.apple.com", "recordit", plist, "GUIDIFY"):
+                    self.fail(msg="Plist parse should have failed: %s" % (title,))
+                
+            plists = (
+                (PlistParse.plist_nomacosxserver_key, "nomacosxserver_key"),
+                (PlistParse.plist_nocalendarservice,  "nocalendarservice"),
+                (PlistParse.plist_noserviceinfo,      "noserviceinfo"),
+                (PlistParse.plist_disabledservice,    "disabledservice"),
+                (PlistParse.plist_nohostname,         "nohostname"),
+                (PlistParse.plist_nohostdetails,      "nohostdetails"),
+                (PlistParse.plist_nocuaddrtemplates,  "nocuaddrtemplates"),
+            )
+            for plist, title in plists:
+                _doParse(plist, title)
+
+        def test_goodplist(self):
+            service = OpenDirectoryService(node="/Search", dosetup=False)
+            if not service._parseXMLPlist("calendar.apple.com", "recordit", PlistParse.plist_good, "GUIDIFY"):
+                self.fail(msg="Plist parse should not have failed")
+            else:
+                # Verify that we extracted the proper items
+                self.assertEqual(service.servicetag, "GUIDIFY:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")
+                self.assertEqual(service.hostvariants, (("http", "calendar.apple.com", 8008), ("https", "calendar.apple.com", 8443)))
+                self.assertEqual(service.cuaddrtemplates, ("%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s", "mailto:%(email)s", "urn:uuid:%(guid)s"))
+
+        def test_expandcuaddrs(self):
+            def _doTest(recordName, record, result, title):
+                service = OpenDirectoryService(node="/Search", dosetup=False)
+                if not service._parseXMLPlist("calendar.apple.com", recordName, PlistParse.plist_good, "GUIDIFY"):
+                    self.fail(msg="Plist parse should not have failed: %s" % (recordName,))
+                else:
+                    expanded = service._templateExpandCalendarUserAddresses(DirectoryService.recordType_users, recordName, record)
+        
+                    # Verify that we extracted the proper items
+                    self.assertEqual(expanded, result, msg=title % (expanded, result,))
+            
+            data = (
+                (
+                 "user01",
+                 {
+                    dsattributes.kDS1AttrGeneratedUID: "GUID-USER-01",
+                    dsattributes.kDSNAttrEMailAddress: "user01 at example.com",
+                 },
+                 set((
+                    "http://calendar.apple.com:8008/principals/users/user01",
+                    "https://calendar.apple.com:8443/principals/users/user01",
+                    "mailto:user01 at example.com",
+                    "urn:uuid:GUID-USER-01",
+                 )),
+                 "User with one email address, %s != %s",
+                ),
+                (
+                 "user02",
+                 {
+                    dsattributes.kDS1AttrGeneratedUID: "GUID-USER-02",
+                    dsattributes.kDSNAttrEMailAddress: ["user02 at example.com", "user02 at calendar.example.com"],
+                 },
+                 set((
+                    "http://calendar.apple.com:8008/principals/users/user02",
+                    "https://calendar.apple.com:8443/principals/users/user02",
+                    "mailto:user02 at example.com",
+                    "mailto:user02 at calendar.example.com",
+                    "urn:uuid:GUID-USER-02",
+                 )),
+                 "User with multiple email addresses, %s != %s",
+                ),
+                (
+                 "user03",
+                 {
+                    dsattributes.kDS1AttrGeneratedUID: "GUID-USER-03",
+                 },
+                 set((
+                    "http://calendar.apple.com:8008/principals/users/user03",
+                    "https://calendar.apple.com:8443/principals/users/user03",
+                    "urn:uuid:GUID-USER-03",
+                 )),
+                 "User with no email addresses, %s != %s",
+                ),
+            )
+            
+            for recordName, record, result, title in data:
+                _doTest(recordName, record, result, title)
+
+    class ODRecordsParse (twisted.trial.unittest.TestCase):
+
+        record_good = ("computer1.apple.com", {
+            dsattributes.kDS1AttrGeneratedUID : "GUID1",
+            dsattributes.kDSNAttrRecordName   : "computer1.apple.com",
+            dsattributes.kDS1AttrXMLPlist     : PlistParse.plist_good,
+        })
+        record_good_other = ("computer2.apple.com", {
+            dsattributes.kDS1AttrGeneratedUID : "GUID1",
+            dsattributes.kDSNAttrRecordName   : "computer2.apple.com",
+            dsattributes.kDS1AttrXMLPlist     : PlistParse.plist_good_other,
+        })
+        record_good_duplicate = ("computer2.apple.com", {
+            dsattributes.kDS1AttrGeneratedUID : "GUID1",
+            dsattributes.kDSNAttrRecordName   : "computer2.apple.com",
+            dsattributes.kDS1AttrXMLPlist     : PlistParse.plist_good,
+        })
+
+        def test_odrecords_error(self):
+            def _doParseRecords(recordlist, title):
+                service = OpenDirectoryService(node="/Search", dosetup=False)
+                try:
+                    service._parseComputersRecords(recordlist, "calendar.apple.com")
+                    self.fail(msg="Record parse should have failed: %s" % (title,))
+                except OpenDirectoryInitError:
+                    pass
+                
+            records = (
+                ({}, "no records found"),
+                ({
+                      ODRecordsParse.record_good[0]            : ODRecordsParse.record_good[1],
+                      ODRecordsParse.record_good_duplicate[0]  : ODRecordsParse.record_good_duplicate[1],
+                 }, "duplicate records found"),
+                ({
+                      ODRecordsParse.record_good_other[0]  : ODRecordsParse.record_good_other[1],
+                 }, "non-matching record found"),
+            )
+
+            for recordlist, title in records:
+                _doParseRecords(recordlist, title)
+
+        def test_odrecords_good(self):
+            def _doParseRecords(recordlist, title):
+                service = OpenDirectoryService(node="/Search", dosetup=False)
+                try:
+                    service._parseComputersRecords(recordlist, "calendar.apple.com")
+                except OpenDirectoryInitError, ex:
+                    self.fail(msg="Record parse should not have failed: \"%s\" with error: %s" % (title, ex))
+                
+            records = (
+                ({
+                      ODRecordsParse.record_good[0]        : ODRecordsParse.record_good[1],
+                 }, "single good plist"),
+                ({
+                      ODRecordsParse.record_good[0]        : ODRecordsParse.record_good[1],
+                      ODRecordsParse.record_good_other[0]  : ODRecordsParse.record_good_other[1],
+                 }, "multiple plists"),
+            )
+
+            for recordlist, title in records:
+                _doParseRecords(recordlist, title)
+

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20070206/2d84096f/attachment.html


More information about the calendarserver-changes mailing list