[CalendarServer-changes] [6561] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Nov 3 12:52:01 PDT 2010


Revision: 6561
          http://trac.macosforge.org/projects/calendarserver/changeset/6561
Author:   sagen at apple.com
Date:     2010-11-03 12:51:58 -0700 (Wed, 03 Nov 2010)
Log Message:
-----------
PyObjC -> OpenDirectory.framework, a replacement for PyOpenDirectory.  It's disabled by default.  Touch /tmp/calendarserver_use_pyobjc to enable.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tools/resources.py
    CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_buildquery.py
    CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py

Added Paths:
-----------
    CalendarServer/trunk/calendarserver/od/
    CalendarServer/trunk/calendarserver/od/__init__.py
    CalendarServer/trunk/calendarserver/od/dsattributes.py
    CalendarServer/trunk/calendarserver/od/dsquery.py
    CalendarServer/trunk/calendarserver/od/opendirectory.py
    CalendarServer/trunk/calendarserver/od/test/
    CalendarServer/trunk/calendarserver/od/test/__init__.py
    CalendarServer/trunk/calendarserver/od/test/setup_directory.py
    CalendarServer/trunk/calendarserver/od/test/test_opendirectory.py

Added: CalendarServer/trunk/calendarserver/od/__init__.py
===================================================================
--- CalendarServer/trunk/calendarserver/od/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/od/__init__.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalendarServer/trunk/calendarserver/od/dsattributes.py
===================================================================
--- CalendarServer/trunk/calendarserver/od/dsattributes.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/od/dsattributes.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -0,0 +1,1894 @@
+##
+# Copyright (c) 2006-2008 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.
+##
+
+"""
+Record types and attribute names from Directory Service.
+This comes directly (with C->Python conversion) from <DirectoryServices/DirServicesConst.h>
+"""
+
+# Specific match types
+
+eDSExact             =    0x2001
+eDSStartsWith        =    0x2002
+eDSEndsWith          =    0x2003
+eDSContains          =    0x2004
+
+eDSLessThan          =    0x2005
+eDSGreaterThan       =    0x2006
+eDSLessEqual         =    0x2007
+eDSGreaterEqual      =    0x2008
+
+# Specific Record Type Constants
+
+"""
+ DirectoryService Specific Record Type Constants
+"""
+ 
+"""
+ kDSStdRecordTypeAccessControls
+  Record type that contains directory access control directives.
+"""
+kDSStdRecordTypeAccessControls = "dsRecTypeStandard:AccessControls"
+
+"""
+ kDSStdRecordTypeAFPServer
+  Record type of AFP server records.
+"""
+kDSStdRecordTypeAFPServer = "dsRecTypeStandard:AFPServer"
+
+"""
+ kDSStdRecordTypeAFPUserAliases
+  Record type of AFP user aliases used exclusively by AFP processes.
+"""
+kDSStdRecordTypeAFPUserAliases = "dsRecTypeStandard:AFPUserAliases"
+
+"""
+ kDSStdRecordTypeAliases
+  Used to represent alias records.
+"""
+kDSStdRecordTypeAliases = "dsRecTypeStandard:Aliases"
+
+"""
+ kDSStdRecordTypeAugments
+  Used to store augmented record data.
+"""
+kDSStdRecordTypeAugments = "dsRecTypeStandard:Augments"
+
+"""
+ kDSStdRecordTypeAutomount
+  Used to store automount record data.
+"""
+kDSStdRecordTypeAutomount = "dsRecTypeStandard:Automount"
+
+"""
+ kDSStdRecordTypeAutomountMap
+  Used to store automountMap record data.
+"""
+kDSStdRecordTypeAutomountMap = "dsRecTypeStandard:AutomountMap"
+
+"""
+ kDSStdRecordTypeAutoServerSetup
+  Used to discover automated server setup information.
+"""
+kDSStdRecordTypeAutoServerSetup = "dsRecTypeStandard:AutoServerSetup"
+
+"""
+ kDSStdRecordTypeBootp
+  Record in the local node for storing bootp info.
+"""
+kDSStdRecordTypeBootp = "dsRecTypeStandard:Bootp"
+
+"""
+ kDSStdRecordTypeCertificateAuthority
+  Record type that contains certificate authority information.
+"""
+kDSStdRecordTypeCertificateAuthorities = "dsRecTypeStandard:CertificateAuthorities"
+
+"""
+ kDSStdRecordTypeComputerLists
+  Identifies computer list records.
+"""
+kDSStdRecordTypeComputerLists = "dsRecTypeStandard:ComputerLists"
+
+"""
+ kDSStdRecordTypeComputerGroups
+  Identifies computer group records.
+"""
+kDSStdRecordTypeComputerGroups = "dsRecTypeStandard:ComputerGroups"
+
+"""
+ kDSStdRecordTypeComputers
+  Identifies computer records.
+"""
+kDSStdRecordTypeComputers = "dsRecTypeStandard:Computers"
+
+"""
+ kDSStdRecordTypeConfig
+  Identifies config records.
+"""
+kDSStdRecordTypeConfig = "dsRecTypeStandard:Config"
+
+"""
+ kDSStdRecordTypeEthernets
+  Record in the local node for storing ethernets.
+"""
+kDSStdRecordTypeEthernets = "dsRecTypeStandard:Ethernets"
+
+"""
+ kDSStdRecordTypeFileMakerServers
+  FileMaker servers record type. Describes available FileMaker servers, 
+  used for service discovery.
+"""
+kDSStdRecordTypeFileMakerServers = "dsRecTypeStandard:FileMakerServers"
+
+"""
+ kDSStdRecordTypeFTPServer
+  Identifies ftp server records.
+"""
+kDSStdRecordTypeFTPServer = "dsRecTypeStandard:FTPServer"
+
+"""
+ kDSStdRecordTypeGroupAliases
+  No longer supported in Mac OS X 10.4 or later.
+"""
+kDSStdRecordTypeGroupAliases = "dsRecTypeStandard:GroupAliases"
+
+"""
+ kDSStdRecordTypeGroups
+  Identifies group records.
+"""
+kDSStdRecordTypeGroups = "dsRecTypeStandard:Groups"
+
+"""
+ kDSStdRecordTypeHostServices
+  Record in the local node for storing host services.
+"""
+kDSStdRecordTypeHostServices = "dsRecTypeStandard:HostServices"
+
+"""
+ kDSStdRecordTypeHosts
+  Identifies host records.
+"""
+kDSStdRecordTypeHosts = "dsRecTypeStandard:Hosts"
+
+"""
+ kDSStdRecordTypeLDAPServer
+  Identifies LDAP server records.
+"""
+kDSStdRecordTypeLDAPServer = "dsRecTypeStandard:LDAPServer"
+
+"""
+ kDSStdRecordTypeLocations
+  Location record type.
+"""
+kDSStdRecordTypeLocations = "dsRecTypeStandard:Locations"
+
+"""
+ kDSStdRecordTypeMachines
+  Identifies machine records.
+"""
+kDSStdRecordTypeMachines = "dsRecTypeStandard:Machines"
+
+"""
+ kDSStdRecordTypeMaps
+  Identifies map records.
+"""
+kDSStdRecordTypeMaps = "dsRecTypeStandard:Maps"
+
+"""
+ kDSStdRecordTypeMeta
+  Identifies meta records.
+"""
+kDSStdRecordTypeMeta = "dsRecTypeStandard:AppleMetaRecord"
+
+"""
+ kDSStdRecordTypeMounts
+  Identifies mount records.
+"""
+kDSStdRecordTypeMounts = "dsRecTypeStandard:Mounts"
+
+"""
+ kDSStdRecordTypMounts
+  Supported only for backward compatibility to kDSStdRecordTypeMounts.
+"""
+kDSStdRecordTypMounts = "dsRecTypeStandard:Mounts"
+
+"""
+ kDSStdRecordTypeNeighborhoods
+  Neighborhood record type. Describes a list of computers and other
+  neighborhoods, used for network browsing.
+"""
+kDSStdRecordTypeNeighborhoods = "dsRecTypeStandard:Neighborhoods"
+
+"""
+ kDSStdRecordTypeNFS
+  Identifies NFS records.
+"""
+kDSStdRecordTypeNFS = "dsRecTypeStandard:NFS"
+
+"""
+ kDSStdRecordTypeNetDomains
+  Record in the local node for storing net domains.
+"""
+kDSStdRecordTypeNetDomains = "dsRecTypeStandard:NetDomains"
+
+"""
+ kDSStdRecordTypeNetGroups
+  Record in the local node for storing net groups.
+"""
+kDSStdRecordTypeNetGroups = "dsRecTypeStandard:NetGroups"
+
+"""
+ kDSStdRecordTypeNetworks
+  Identifies network records.
+"""
+kDSStdRecordTypeNetworks = "dsRecTypeStandard:Networks"
+
+"""
+ kDSStdRecordTypePasswordServer
+  Used to discover password servers via Bonjour.
+"""
+kDSStdRecordTypePasswordServer = "dsRecTypeStandard:PasswordServer"
+
+"""
+ kDSStdRecordTypePeople
+  Record type that contains "People" records used for contact information.
+"""
+kDSStdRecordTypePeople = "dsRecTypeStandard:People"
+
+"""
+ kDSStdRecordTypePlaces
+  Identifies places (rooms) records.
+"""
+kDSStdRecordTypePlaces = "dsRecTypeStandard:Places"
+
+"""
+ kDSStdRecordTypePresetComputers
+  The computer record type used for presets in record creation.
+"""
+kDSStdRecordTypePresetComputers = "dsRecTypeStandard:PresetComputers"
+
+"""
+ kDSStdRecordTypePresetComputerGroups
+  The computer group record type used for presets in record creation.
+"""
+kDSStdRecordTypePresetComputerGroups = "dsRecTypeStandard:PresetComputerGroups"
+
+"""
+ kDSStdRecordTypePresetComputerLists
+  The computer list record type used for presets in record creation.
+"""
+kDSStdRecordTypePresetComputerLists = "dsRecTypeStandard:PresetComputerLists"
+
+"""
+ kDSStdRecordTypePresetGroups
+  The group record type used for presets in record creation.
+"""
+kDSStdRecordTypePresetGroups = "dsRecTypeStandard:PresetGroups"
+
+"""
+ kDSStdRecordTypePresetUsers
+  The user record type used for presets in record creation.
+"""
+kDSStdRecordTypePresetUsers = "dsRecTypeStandard:PresetUsers"
+
+"""
+ kDSStdRecordTypePrintService
+  Identifies print service records.
+"""
+kDSStdRecordTypePrintService = "dsRecTypeStandard:PrintService"
+
+"""
+ kDSStdRecordTypePrintServiceUser
+  Record in the local node for storing quota usage for a user.
+"""
+kDSStdRecordTypePrintServiceUser = "dsRecTypeStandard:PrintServiceUser"
+
+"""
+ kDSStdRecordTypePrinters
+  Identifies printer records.
+"""
+kDSStdRecordTypePrinters = "dsRecTypeStandard:Printers"
+
+"""
+ kDSStdRecordTypeProtocols
+  Identifies protocol records.
+"""
+kDSStdRecordTypeProtocols = "dsRecTypeStandard:Protocols"
+
+"""
+ kDSStdRecordTypProtocols
+  Supported only for backward compatibility to kDSStdRecordTypeProtocols.
+"""
+kDSStdRecordTypProtocols = "dsRecTypeStandard:Protocols"
+
+"""
+ kDSStdRecordTypeQTSServer
+  Identifies quicktime streaming server records.
+"""
+kDSStdRecordTypeQTSServer = "dsRecTypeStandard:QTSServer"
+
+"""
+ kDSStdRecordTypeResources
+  Identifies resources used in group services.
+"""
+kDSStdRecordTypeResources = "dsRecTypeStandard:Resources"
+
+"""
+ kDSStdRecordTypeRPC
+  Identifies remote procedure call records.
+"""
+kDSStdRecordTypeRPC = "dsRecTypeStandard:RPC"
+
+"""
+ kDSStdRecordTypRPC
+  Supported only for backward compatibility to kDSStdRecordTypeRPC.
+"""
+kDSStdRecordTypRPC = "dsRecTypeStandard:RPC"
+
+"""
+ kDSStdRecordTypeSMBServer
+  Identifies SMB server records.
+"""
+kDSStdRecordTypeSMBServer = "dsRecTypeStandard:SMBServer"
+
+"""
+ kDSStdRecordTypeServer
+  Identifies generic server records.
+"""
+kDSStdRecordTypeServer = "dsRecTypeStandard:Server"
+
+"""
+ kDSStdRecordTypeServices
+  Identifies directory based service records.
+"""
+kDSStdRecordTypeServices = "dsRecTypeStandard:Services"
+
+"""
+ kDSStdRecordTypeSharePoints
+  Share point record type.
+"""
+kDSStdRecordTypeSharePoints = "dsRecTypeStandard:SharePoints"
+
+"""
+ kDSStdRecordTypeUserAliases
+  No longer supported in Mac OS X 10.4 or later.
+"""
+kDSStdRecordTypeUserAliases = "dsRecTypeStandard:UserAliases"
+
+"""
+ kDSStdRecordTypeUsers
+  Identifies user records.
+"""
+kDSStdRecordTypeUsers = "dsRecTypeStandard:Users"
+
+"""
+ kDSStdRecordTypeWebServer
+  Identifies web server records.
+"""
+kDSStdRecordTypeWebServer = "dsRecTypeStandard:WebServer"
+
+# Specific Attribute Type Constants
+
+
+"""
+ DirectoryService Specific Attribute Type Constants
+ As a guideline for the attribute types the following legend is used:
+
+        eDS1xxxxxx  Single Valued Attribute
+
+        eDSNxxxxxx  Multi-Valued Attribute
+
+    NOTE #1: Access controls may prevent any particular client from reading/writting
+            various attribute types.  In addition some attribute types may not be stored at
+            all and could also represent "real-time" data generated by the directory node
+            plug-in.
+
+    NOTE #2: Attributes in the model are available for records and directory nodes.
+"""
+ 
+
+# Single Valued Specific Attribute Type Constants
+
+
+"""
+ DirectoryService Single Valued Specific Attribute Type Constants
+"""
+
+"""
+    kDS1AttrAdminLimits
+    XML plist indicating what an admin user can edit.
+        Found in kDSStdRecordTypeUsers records.
+"""
+kDS1AttrAdminLimits = "dsAttrTypeStandard:AdminLimits"
+
+"""
+ kDS1AttrAliasData
+ Used to identify alias data.
+"""
+kDS1AttrAliasData = "dsAttrTypeStandard:AppleAliasData"
+
+"""
+ kDS1AttrAlternateDatastoreLocation
+ Unix path used for determining where a user's email is stored.
+"""
+kDS1AttrAlternateDatastoreLocation = "dsAttrTypeStandard:AlternateDatastoreLocation"
+
+"""
+ kDS1AttrAuthenticationHint
+ Used to identify the authentication hint phrase.
+"""
+kDS1AttrAuthenticationHint = "dsAttrTypeStandard:AuthenticationHint"
+
+"""
+ kDSNAttrAttributeTypes
+ Used to indicated recommended attribute types for a record type in the Config node.
+"""
+kDSNAttrAttributeTypes = "dsAttrTypeStandard:AttributeTypes"
+
+"""
+ kDS1AttrAuthorityRevocationList
+ Attribute containing the binary of the authority revocation list.
+ A certificate revocation list that defines certificate authority certificates
+ which are no longer trusted.  No user certificates are included in this list.
+ Usually found in kDSStdRecordTypeCertificateAuthority records.
+"""
+kDS1AttrAuthorityRevocationList = "dsAttrTypeStandard:AuthorityRevocationList"
+
+"""
+ kDS1AttrBirthday
+ Single-valued attribute that defines the user's birthday.
+ Format is x.208 standard YYYYMMDDHHMMSSZ which we will require as GMT time.
+"""
+kDS1AttrBirthday = "dsAttrTypeStandard:Birthday"
+
+
+"""
+ kDS1AttrBootFile
+ Attribute type in host or machine records for the name of the 
+        kernel that this machine will use by default when NetBooting.
+"""
+kDS1AttrBootFile = "dsAttrTypeStandard:BootFile"
+
+"""
+ kDS1AttrCACertificate
+ Attribute containing the binary of the certificate of a
+ certificate authority. Its corresponding private key is used to sign certificates.
+ Usually found in kDSStdRecordTypeCertificateAuthority records.
+"""
+kDS1AttrCACertificate = "dsAttrTypeStandard:CACertificate"
+
+"""
+ kDS1AttrCapabilities
+ Used with directory nodes so that clients can "discover" the
+ API capabilities for this Directory Node.
+"""
+kDS1AttrCapabilities = "dsAttrTypeStandard:Capabilities"
+
+"""
+ kDS1AttrCapacity
+ Attribute type for the capacity of a resource. 
+     found in resource records (kDSStdRecordTypeResources). 
+    Example: 50
+"""
+kDS1AttrCapacity = "dsAttrTypeStandard:Capacity"
+
+"""
+    kDS1AttrCategory
+    The category of an item used for browsing
+"""
+kDS1AttrCategory = "dsAttrTypeStandard:Category"
+
+"""
+ kDS1AttrCertificateRevocationList
+ Attribute containing the binary of the certificate revocation list.
+ This is a list of certificates which are no longer trusted.
+ Usually found in kDSStdRecordTypeCertificateAuthority records.
+"""
+kDS1AttrCertificateRevocationList = "dsAttrTypeStandard:CertificateRevocationList"
+
+"""
+ kDS1AttrChange
+ Retained for backward compatibility.
+"""
+kDS1AttrChange = "dsAttrTypeStandard:Change"
+
+"""
+ kDS1AttrComment
+ Attribute used for unformatted comment.
+"""
+kDS1AttrComment = "dsAttrTypeStandard:Comment"
+
+"""
+ kDS1AttrContactGUID
+ Attribute type for the contact GUID of a group. 
+     found in group records (kDSStdRecordTypeGroups). 
+"""
+kDS1AttrContactGUID = "dsAttrTypeStandard:ContactGUID"
+
+"""
+ kDS1AttrContactPerson
+ Attribute type for the contact person of the machine. 
+        Found in host or machine records.
+"""
+kDS1AttrContactPerson = "dsAttrTypeStandard:ContactPerson"
+
+"""
+ kDS1AttrCreationTimestamp
+ Attribute showing date/time of record creation.
+ Format is x.208 standard YYYYMMDDHHMMSSZ which we will require as GMT time.
+"""
+kDS1AttrCreationTimestamp = "dsAttrTypeStandard:CreationTimestamp"
+
+"""
+ kDS1AttrCrossCertificatePair
+ Attribute containing the binary of a pair of certificates which 
+ verify each other.  Both certificates have the same level of authority.
+ Usually found in kDSStdRecordTypeCertificateAuthority records.
+"""
+kDS1AttrCrossCertificatePair = "dsAttrTypeStandard:CrossCertificatePair"
+
+"""
+ kDS1AttrDataStamp
+ checksum/meta data
+"""
+kDS1AttrDataStamp = "dsAttrTypeStandard:DataStamp"
+
+"""
+ kDS1AttrDistinguishedName
+ Users distinguished or real name
+"""
+kDS1AttrDistinguishedName = "dsAttrTypeStandard:RealName"
+
+"""
+ kDS1AttrDNSDomain
+ DNS Resolver domain attribute.
+"""
+kDS1AttrDNSDomain = "dsAttrTypeStandard:DNSDomain"
+
+"""
+ kDS1AttrDNSNameServer
+ DNS Resolver nameserver attribute.
+"""
+kDS1AttrDNSNameServer = "dsAttrTypeStandard:DNSNameServer"
+
+"""
+ kDS1AttrENetAddress
+ Single-valued attribute for hardware Ethernet address (MAC address).
+        Found in machine records (kDSStdRecordTypeMachines) and computer records
+        (kDSStdRecordTypeComputers).
+"""
+kDS1AttrENetAddress = "dsAttrTypeStandard:ENetAddress"
+
+"""
+ kDS1AttrExpire
+ Used for expiration date or time depending on association.
+"""
+kDS1AttrExpire = "dsAttrTypeStandard:Expire"
+
+"""
+ kDS1AttrFirstName
+ Used for first name of user or person record.
+"""
+kDS1AttrFirstName = "dsAttrTypeStandard:FirstName"
+
+"""
+ kDS1AttrGeneratedUID
+ Used for 36 character (128 bit) unique ID. Usually found in user, 
+ group, and computer records. An example value is "A579E95E-CDFE-4EBC-B7E7-F2158562170F".
+ The standard format contains 32 hex characters and four hyphen characters.
+"""
+kDS1AttrGeneratedUID = "dsAttrTypeStandard:GeneratedUID"
+
+"""
+    kDS1AttrHomeDirectoryQuota
+    Represents the allowed usage for a user's home directory in bytes.
+        Found in user records (kDSStdRecordTypeUsers).
+"""
+kDS1AttrHomeDirectoryQuota = "dsAttrTypeStandard:HomeDirectoryQuota"
+
+"""
+ kDS1AttrHomeDirectorySoftQuota
+ Used to define home directory size limit in bytes when user is notified
+ that the hard limit is approaching.
+"""
+kDS1AttrHomeDirectorySoftQuota = "dsAttrTypeStandard:HomeDirectorySoftQuota"
+
+"""
+    kDS1AttrHomeLocOwner
+    Represents the owner of a workgroup's shared home directory.
+        Typically found in kDSStdRecordTypeGroups records.
+"""
+kDS1AttrHomeLocOwner = "dsAttrTypeStandard:HomeLocOwner"
+
+"""
+    kDS1StandardAttrHomeLocOwner
+    Retained for backward compatibility.
+"""
+kDS1StandardAttrHomeLocOwner = kDS1AttrHomeLocOwner
+
+"""
+ kDS1AttrInternetAlias
+ Used to track internet alias.
+"""
+kDS1AttrInternetAlias = "dsAttrTypeStandard:InetAlias"
+
+"""
+ kDS1AttrKDCConfigData
+ Contents of the kdc.conf file.
+"""
+kDS1AttrKDCConfigData = "dsAttrTypeStandard:KDCConfigData"
+
+"""
+ kDS1AttrLastName
+ Used for the last name of user or person record.
+"""
+kDS1AttrLastName = "dsAttrTypeStandard:LastName"
+
+"""
+ kDS1AttrLDAPSearchBaseSuffix
+ Search base suffix for a LDAP server.
+"""
+kDS1AttrLDAPSearchBaseSuffix = "dsAttrTypeStandard:LDAPSearchBaseSuffix"
+
+"""
+ kDS1AttrLocation
+ Represents the location a service is available from (usually domain name).
+     Typically found in service record types including kDSStdRecordTypeAFPServer,
+     kDSStdRecordTypeLDAPServer, and kDSStdRecordTypeWebServer.
+"""
+kDS1AttrLocation = "dsAttrTypeStandard:Location"
+
+"""
+ kDS1AttrMapGUID
+ Represents the GUID for a record's map.
+"""
+kDS1AttrMapGUID = "dsAttrTypeStandard:MapGUID"
+
+"""
+ kDS1AttrMCXFlags
+ Used by MCX.
+"""
+kDS1AttrMCXFlags = "dsAttrTypeStandard:MCXFlags"
+
+"""
+ kDS1AttrMCXSettings
+ Used by MCX.
+"""
+kDS1AttrMCXSettings = "dsAttrTypeStandard:MCXSettings"
+
+"""
+ kDS1AttrMailAttribute
+ Holds the mail account config data.
+"""
+kDS1AttrMailAttribute = "dsAttrTypeStandard:MailAttribute"
+
+"""
+ kDS1AttrMetaAutomountMap
+ Used to query for kDSStdRecordTypeAutomount entries associated with a specific 
+ kDSStdRecordTypeAutomountMap.
+"""
+kDS1AttrMetaAutomountMap = "dsAttrTypeStandard:MetaAutomountMap"
+
+"""
+ kDS1AttrMiddleName
+ Used for the middle name of user or person record.
+"""
+kDS1AttrMiddleName = "dsAttrTypeStandard:MiddleName"
+
+"""
+ kDS1AttrModificationTimestamp
+ Attribute showing date/time of record modification.
+ Format is x.208 standard YYYYMMDDHHMMSSZ which we will require as GMT time.
+"""
+kDS1AttrModificationTimestamp = "dsAttrTypeStandard:ModificationTimestamp"
+
+"""
+ kDSNAttrNeighborhoodAlias
+ Attribute type in Neighborhood records describing sub-neighborhood records.
+"""
+kDSNAttrNeighborhoodAlias = "dsAttrTypeStandard:NeighborhoodAlias"
+
+"""
+ kDS1AttrNeighborhoodType
+ Attribute type in Neighborhood records describing their function.
+"""
+kDS1AttrNeighborhoodType = "dsAttrTypeStandard:NeighborhoodType"
+
+"""
+ kDS1AttrNetworkView
+ The name of the managed network view a computer should use for browsing.
+"""
+kDS1AttrNetworkView = "dsAttrTypeStandard:NetworkView"
+
+"""
+ kDS1AttrNFSHomeDirectory
+ Defines a user's home directory mount point on the local machine.
+"""
+kDS1AttrNFSHomeDirectory = "dsAttrTypeStandard:NFSHomeDirectory"
+
+"""
+ kDS1AttrNote
+ Note attribute. Commonly used in printer records.
+"""
+kDS1AttrNote = "dsAttrTypeStandard:Note"
+
+"""
+ kDS1AttrOwner
+ Attribute type for the owner of a record. 
+        Typically the value is a LDAP distinguished name.
+"""
+kDS1AttrOwner = "dsAttrTypeStandard:Owner"
+
+"""
+ kDS1AttrOwnerGUID
+ Attribute type for the owner GUID of a group. 
+     found in group records (kDSStdRecordTypeGroups). 
+"""
+kDS1AttrOwnerGUID = "dsAttrTypeStandard:OwnerGUID"
+
+"""
+ kDS1AttrPassword
+ Holds the password or credential value.
+"""
+kDS1AttrPassword = "dsAttrTypeStandard:Password"
+
+"""
+ kDS1AttrPasswordPlus
+ Holds marker data to indicate possible authentication redirection.
+"""
+kDS1AttrPasswordPlus = "dsAttrTypeStandard:PasswordPlus"
+
+"""
+ kDS1AttrPasswordPolicyOptions
+ Collection of password policy options in single attribute.
+ Used in user presets record.
+"""
+kDS1AttrPasswordPolicyOptions = "dsAttrTypeStandard:PasswordPolicyOptions"
+
+"""
+ kDS1AttrPasswordServerList
+ Represents the attribute for storing the password server's replication information.
+"""
+kDS1AttrPasswordServerList = "dsAttrTypeStandard:PasswordServerList"
+
+"""
+    kDS1AttrPasswordServerLocation
+    Specifies the IP address or domain name of the Password Server associated
+        with a given directory node. Found in a config record named PasswordServer.
+"""
+kDS1AttrPasswordServerLocation = "dsAttrTypeStandard:PasswordServerLocation"
+
+"""
+ kDS1AttrPicture
+ Represents the path of the picture for each user displayed in the login window.
+ Found in user records (kDSStdRecordTypeUsers).
+"""
+kDS1AttrPicture = "dsAttrTypeStandard:Picture"
+
+"""
+ kDS1AttrPort
+ Represents the port number a service is available on.
+     Typically found in service record types including kDSStdRecordTypeAFPServer,
+     kDSStdRecordTypeLDAPServer, and kDSStdRecordTypeWebServer.
+"""
+kDS1AttrPort = "dsAttrTypeStandard:Port"
+
+"""
+    kDS1AttrPresetUserIsAdmin
+    Flag to indicate whether users created from this preset are administrators
+        by default. Found in kDSStdRecordTypePresetUsers records.
+"""
+kDS1AttrPresetUserIsAdmin = "dsAttrTypeStandard:PresetUserIsAdmin"
+
+"""
+ kDS1AttrPrimaryComputerGUID
+ Single-valued attribute that defines a primary computer of the computer group.  
+ added via extensible object for computer group record type (kDSStdRecordTypeComputerGroups)
+"""
+kDS1AttrPrimaryComputerGUID = "dsAttrTypeStandard:PrimaryComputerGUID"
+
+"""
+ kDS1AttrPrimaryComputerList
+ The GUID of the computer list with which this computer record is associated.
+"""
+kDS1AttrPrimaryComputerList = "dsAttrTypeStandard:PrimaryComputerList"
+
+"""
+ kDS1AttrPrimaryGroupID
+ This is the 32 bit unique ID that represents the primary group 
+ a user is part of, or the ID of a group. Format is a signed 32 bit integer
+ represented as a string.
+"""
+kDS1AttrPrimaryGroupID = "dsAttrTypeStandard:PrimaryGroupID"
+
+"""
+ kDS1AttrPrinter1284DeviceID
+ Single-valued attribute that defines the IEEE 1284 DeviceID of a printer.
+              This is used when configuring a printer.
+"""
+kDS1AttrPrinter1284DeviceID = "dsAttrTypeStandard:Printer1284DeviceID"
+
+"""
+ kDS1AttrPrinterLPRHost
+ Standard attribute type for kDSStdRecordTypePrinters.
+"""
+kDS1AttrPrinterLPRHost = "dsAttrTypeStandard:PrinterLPRHost"
+
+"""
+ kDS1AttrPrinterLPRQueue
+ Standard attribute type for kDSStdRecordTypePrinters.
+"""
+kDS1AttrPrinterLPRQueue = "dsAttrTypeStandard:PrinterLPRQueue"
+
+"""
+ kDS1AttrPrinterMakeAndModel
+ Single-valued attribute for definition of the Printer Make and Model.  An example
+              Value would be "HP LaserJet 2200".  This would be used to determine the proper PPD
+              file to be used when configuring a printer from the Directory.  This attribute
+              is based on the IPP Printing Specification RFC and IETF IPP-LDAP Printer Record.
+"""
+kDS1AttrPrinterMakeAndModel = "dsAttrTypeStandard:PrinterMakeAndModel"
+
+"""
+ kDS1AttrPrinterType
+ Standard attribute type for kDSStdRecordTypePrinters.
+"""
+kDS1AttrPrinterType = "dsAttrTypeStandard:PrinterType"
+
+"""
+ kDS1AttrPrinterURI
+ Single-valued attribute that defines the URI of a printer "ipp://address" or
+              "smb://server/queue".  This is used when configuring a printer. This attribute
+                is based on the IPP Printing Specification RFC and IETF IPP-LDAP Printer Record.
+"""
+kDS1AttrPrinterURI = "dsAttrTypeStandard:PrinterURI"
+
+"""
+ kDS1AttrPrinterXRISupported
+ Multi-valued attribute that defines additional URIs supported by a printer.
+              This is used when configuring a printer. This attribute is based on the IPP 
+                Printing Specification RFC and IETF IPP-LDAP Printer Record.
+"""
+kDSNAttrPrinterXRISupported = "dsAttrTypeStandard:PrinterXRISupported"
+
+"""
+ kDS1AttrPrintServiceInfoText
+ Standard attribute type for kDSStdRecordTypePrinters.
+"""
+kDS1AttrPrintServiceInfoText = "dsAttrTypeStandard:PrintServiceInfoText"
+
+"""
+ kDS1AttrPrintServiceInfoXML
+ Standard attribute type for kDSStdRecordTypePrinters.
+"""
+kDS1AttrPrintServiceInfoXML = "dsAttrTypeStandard:PrintServiceInfoXML"
+
+"""
+ kDS1AttrPrintServiceUserData
+ Single-valued attribute for print quota configuration or statistics
+        (XML data). Found in user records (kDSStdRecordTypeUsers) or print service
+        statistics records (kDSStdRecordTypePrintServiceUser).
+"""
+kDS1AttrPrintServiceUserData = "dsAttrTypeStandard:PrintServiceUserData"
+
+"""
+ kDS1AttrRealUserID
+ Used by MCX.
+"""
+kDS1AttrRealUserID = "dsAttrTypeStandard:RealUserID"
+
+"""
+ kDS1AttrRelativeDNPrefix
+ Used to map the first native LDAP attribute type required in the building of the
+  Relative Distinguished Name for LDAP record creation.
+"""
+kDS1AttrRelativeDNPrefix = "dsAttrTypeStandard:RelativeDNPrefix"
+
+"""
+ kDS1AttrSMBAcctFlags
+ Account control flag.
+"""
+kDS1AttrSMBAcctFlags = "dsAttrTypeStandard:SMBAccountFlags"
+
+"""
+ kDS1AttrSMBGroupRID
+ Constant for supporting PDC SMB interaction with DS.
+"""
+kDS1AttrSMBGroupRID = "dsAttrTypeStandard:SMBGroupRID"
+
+"""
+ kDS1AttrSMBHome
+ 
+     UNC address of Windows homedirectory mount point (\\server\\sharepoint).
+"""
+kDS1AttrSMBHome = "dsAttrTypeStandard:SMBHome"
+
+"""
+ kDS1AttrSMBHomeDrive
+ 
+     Drive letter for homedirectory mount point.
+"""
+kDS1AttrSMBHomeDrive = "dsAttrTypeStandard:SMBHomeDrive"
+
+"""
+ kDS1AttrSMBKickoffTime
+ Attribute in support of SMB interaction.
+"""
+kDS1AttrSMBKickoffTime = "dsAttrTypeStandard:SMBKickoffTime"
+
+"""
+ kDS1AttrSMBLogoffTime
+ Attribute in support of SMB interaction.
+"""
+kDS1AttrSMBLogoffTime = "dsAttrTypeStandard:SMBLogoffTime"
+
+"""
+ kDS1AttrSMBLogonTime
+ Attribute in support of SMB interaction.
+"""
+kDS1AttrSMBLogonTime = "dsAttrTypeStandard:SMBLogonTime"
+
+"""
+ kDS1AttrSMBPrimaryGroupSID
+ SMB Primary Group Security ID, stored as a string attribute of
+    up to 64 bytes. Found in user, group, and computer records
+    (kDSStdRecordTypeUsers, kDSStdRecordTypeGroups, kDSStdRecordTypeComputers).
+"""
+kDS1AttrSMBPrimaryGroupSID = "dsAttrTypeStandard:SMBPrimaryGroupSID"
+
+"""
+ kDS1AttrSMBPWDLastSet
+ Attribute in support of SMB interaction.
+"""
+kDS1AttrSMBPWDLastSet = "dsAttrTypeStandard:SMBPasswordLastSet"
+
+"""
+ kDS1AttrSMBProfilePath
+ Desktop management info (dock, desktop links, etc).
+"""
+kDS1AttrSMBProfilePath = "dsAttrTypeStandard:SMBProfilePath"
+
+"""
+ kDS1AttrSMBRID
+ Attribute in support of SMB interaction.
+"""
+kDS1AttrSMBRID = "dsAttrTypeStandard:SMBRID"
+
+"""
+ kDS1AttrSMBScriptPath
+ Login script path.
+"""
+kDS1AttrSMBScriptPath = "dsAttrTypeStandard:SMBScriptPath"
+
+"""
+ kDS1AttrSMBSID
+ SMB Security ID, stored as a string attribute of up to 64 bytes.
+    Found in user, group, and computer records (kDSStdRecordTypeUsers, 
+    kDSStdRecordTypeGroups, kDSStdRecordTypeComputers).
+"""
+kDS1AttrSMBSID = "dsAttrTypeStandard:SMBSID"
+
+"""
+ kDS1AttrSMBUserWorkstations
+ List of workstations user can login from (machine account names).
+"""
+kDS1AttrSMBUserWorkstations = "dsAttrTypeStandard:SMBUserWorkstations"
+
+"""
+ kDS1AttrServiceType
+ Represents the service type for the service.  This is the raw service type of the
+     service.  For example a service record type of kDSStdRecordTypeWebServer 
+     might have a service type of "http" or "https".
+"""
+kDS1AttrServiceType = "dsAttrTypeStandard:ServiceType"
+
+"""
+ kDS1AttrSetupAdvertising
+ Used for Setup Assistant automatic population.
+"""
+kDS1AttrSetupAdvertising = "dsAttrTypeStandard:SetupAssistantAdvertising"
+
+"""
+ kDS1AttrSetupAutoRegister
+ Used for Setup Assistant automatic population.
+"""
+kDS1AttrSetupAutoRegister = "dsAttrTypeStandard:SetupAssistantAutoRegister"
+
+"""
+ kDS1AttrSetupLocation
+ Used for Setup Assistant automatic population.
+"""
+kDS1AttrSetupLocation = "dsAttrTypeStandard:SetupAssistantLocation"
+
+"""
+ kDS1AttrSetupOccupation
+ Used for Setup Assistant automatic population.
+"""
+kDS1AttrSetupOccupation = "dsAttrTypeStandard:Occupation"
+
+"""
+ kDS1AttrTimeToLive
+ Attribute recommending how long to cache the record's attribute values.
+ Format is an unsigned 32 bit representing seconds. ie. 300 is 5 minutes.
+"""
+kDS1AttrTimeToLive = "dsAttrTypeStandard:TimeToLive"
+
+"""
+ kDS1AttrUniqueID
+ This is the 32 bit unique ID that represents the user in the legacy manner.
+ Format is a signed integer represented as a string.
+"""
+kDS1AttrUniqueID = "dsAttrTypeStandard:UniqueID"
+
+"""
+ kDS1AttrUserCertificate
+ Attribute containing the binary of the user's certificate.
+ Usually found in user records. The certificate is data which identifies a user.
+ This data is attested to by a known party, and can be independently verified 
+ by a third party.
+"""
+kDS1AttrUserCertificate = "dsAttrTypeStandard:UserCertificate"
+
+"""
+ kDS1AttrUserPKCS12Data
+ Attribute containing binary data in PKCS #12 format. 
+ Usually found in user records. The value can contain keys, certificates,
+ and other related information and is encrypted with a passphrase.
+"""
+kDS1AttrUserPKCS12Data = "dsAttrTypeStandard:UserPKCS12Data"
+
+"""
+ kDS1AttrUserShell
+ Used to represent the user's shell setting.
+"""
+kDS1AttrUserShell = "dsAttrTypeStandard:UserShell"
+
+"""
+ kDS1AttrUserSMIMECertificate
+ Attribute containing the binary of the user's SMIME certificate.
+ Usually found in user records. The certificate is data which identifies a user.
+ This data is attested to by a known party, and can be independently verified 
+ by a third party. SMIME certificates are often used for signed or encrypted
+ emails.
+"""
+kDS1AttrUserSMIMECertificate = "dsAttrTypeStandard:UserSMIMECertificate"
+
+"""
+ kDS1AttrVFSDumpFreq
+ Attribute used to support mount records.
+"""
+kDS1AttrVFSDumpFreq = "dsAttrTypeStandard:VFSDumpFreq"
+
+"""
+ kDS1AttrVFSLinkDir
+ Attribute used to support mount records.
+"""
+kDS1AttrVFSLinkDir = "dsAttrTypeStandard:VFSLinkDir"
+
+"""
+ kDS1AttrVFSPassNo
+ Attribute used to support mount records.
+"""
+kDS1AttrVFSPassNo = "dsAttrTypeStandard:VFSPassNo"
+
+"""
+ kDS1AttrVFSType
+ Attribute used to support mount records.
+"""
+kDS1AttrVFSType = "dsAttrTypeStandard:VFSType"
+
+"""
+ kDS1AttrWeblogURI
+ Single-valued attribute that defines the URI of a user's weblog.
+    Usually found in user records (kDSStdRecordTypeUsers). 
+    Example: http://example.com/blog/jsmith
+"""
+kDS1AttrWeblogURI = "dsAttrTypeStandard:WeblogURI"
+
+"""
+    kDS1AttrXMLPlist
+    SA config settings plist.
+"""
+kDS1AttrXMLPlist = "dsAttrTypeStandard:XMLPlist"
+
+"""
+ kDS1AttrProtocolNumber
+ Single-valued attribute that defines a protocol number.  Usually found
+  in protocol records (kDSStdRecordTypeProtocols)
+"""
+kDS1AttrProtocolNumber = "dsAttrTypeStandard:ProtocolNumber"
+
+"""
+ kDS1AttrRPCNumber
+ Single-valued attribute that defines an RPC number.  Usually found
+  in RPC records (kDSStdRecordTypeRPC)
+"""
+kDS1AttrRPCNumber = "dsAttrTypeStandard:RPCNumber"
+
+"""
+ kDS1AttrNetworkNumber
+ Single-valued attribute that defines a network number.  Usually found
+  in network records (kDSStdRecordTypeNetworks)
+"""
+kDS1AttrNetworkNumber = "dsAttrTypeStandard:NetworkNumber"
+
+
+# Multiple Valued Specific Attribute Type Constants
+
+
+"""
+ DirectoryService Multiple Valued Specific Attribute Type Constants
+"""
+
+"""
+ kDSNAttrAccessControlEntry
+ Attribute type which stores directory access control directives.
+"""
+kDSNAttrAccessControlEntry = "dsAttrTypeStandard:AccessControlEntry"
+
+"""
+ kDSNAttrAddressLine1
+ Line one of multiple lines of address data for a user.
+"""
+kDSNAttrAddressLine1 = "dsAttrTypeStandard:AddressLine1"
+
+"""
+ kDSNAttrAddressLine2
+ Line two of multiple lines of address data for a user.
+"""
+kDSNAttrAddressLine2 = "dsAttrTypeStandard:AddressLine2"
+
+"""
+ kDSNAttrAddressLine3
+ Line three of multiple lines of address data for a user.
+"""
+kDSNAttrAddressLine3 = "dsAttrTypeStandard:AddressLine3"
+
+"""
+ kDSNAttrAltSecurityIdentities
+ Alternative Security Identities such as Kerberos principals.
+"""
+kDSNAttrAltSecurityIdentities = "dsAttrTypeStandard:AltSecurityIdentities"
+
+"""
+ kDSNAttrAreaCode
+ Area code of a user's phone number.
+"""
+kDSNAttrAreaCode = "dsAttrTypeStandard:AreaCode"
+
+"""
+ kDSNAttrAuthenticationAuthority
+ Determines what mechanism is used to verify or set a user's password.
+     If multiple values are present, the first attributes returned take precedence.
+     Typically found in User records (kDSStdRecordTypeUsers).
+"""
+kDSNAttrAuthenticationAuthority = "dsAttrTypeStandard:AuthenticationAuthority"
+
+"""
+ kDSNAttrAutomountInformation
+ Used to store automount information in kDSStdRecordTypeAutomount records.
+"""
+kDSNAttrAutomountInformation = "dsAttrTypeStandard:AutomountInformation"
+
+"""
+ kDSNAttrBootParams
+ Attribute type in host or machine records for storing boot params.
+"""
+kDSNAttrBootParams = "dsAttrTypeStandard:BootParams"
+
+"""
+ kDSNAttrBuilding
+ Represents the building name for a user or person record.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrBuilding = "dsAttrTypeStandard:Building"
+
+"""
+ kDSNAttrCalendarPrincipalURI
+ the URI for a record's calendar
+"""
+kDSNAttrCalendarPrincipalURI = "dsAttrTypeStandard:CalendarPrincipalURI"
+
+"""
+ kDSNAttrCity
+ Usually, city for a user or person record.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrCity = "dsAttrTypeStandard:City"
+
+"""
+ kDSNAttrCompany
+ attribute that defines the user's company.
+ Example: Apple Computer, Inc
+"""
+kDSNAttrCompany = "dsAttrTypeStandard:Company"
+
+"""
+ kDSNAttrComputerAlias
+ Attribute type in Neighborhood records describing computer records pointed to by
+ this neighborhood.
+"""
+kDSNAttrComputerAlias = "dsAttrTypeStandard:ComputerAlias"
+
+"""
+ kDSNAttrComputers
+ List of computers.
+"""
+kDSNAttrComputers = "dsAttrTypeStandard:Computers"
+
+"""
+ kDSNAttrCountry
+ Represents country of a record entry.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrCountry = "dsAttrTypeStandard:Country"
+
+"""
+ kDSNAttrDepartment
+ Represents the department name of a user or person.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrDepartment = "dsAttrTypeStandard:Department"
+
+"""
+ kDSNAttrDNSName
+ Domain Name Service name.
+"""
+kDSNAttrDNSName = "dsAttrTypeStandard:DNSName"
+
+"""
+ kDSNAttrEMailAddress
+ Email address of usually a user record.
+"""
+kDSNAttrEMailAddress = "dsAttrTypeStandard:EMailAddress"
+
+"""
+ kDSNAttrEMailContacts
+ multi-valued attribute that defines a record's custom email addresses .
+     found in user records (kDSStdRecordTypeUsers). 
+    Example: home:johndoe at mymail.com
+"""
+kDSNAttrEMailContacts = "dsAttrTypeStandard:EMailContacts"
+
+"""
+ kDSNAttrFaxNumber
+ Represents the FAX numbers of a user or person.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrFaxNumber = "dsAttrTypeStandard:FAXNumber"
+
+"""
+ kDSNAttrGroup
+ List of groups.
+"""
+kDSNAttrGroup = "dsAttrTypeStandard:Group"
+
+"""
+ kDSNAttrGroupMembers
+ Attribute type in group records containing lists of GUID values for members other than groups.
+"""
+kDSNAttrGroupMembers = "dsAttrTypeStandard:GroupMembers"
+
+"""
+ kDSNAttrGroupMembership
+ Usually a list of users that below to a given group record.
+"""
+kDSNAttrGroupMembership = "dsAttrTypeStandard:GroupMembership"
+
+"""
+ kDSNAttrGroupServices
+ xml-plist attribute that defines a group's services .
+     found in group records (kDSStdRecordTypeGroups). 
+"""
+kDSNAttrGroupServices = "dsAttrTypeStandard:GroupServices"
+
+"""
+ kDSNAttrHomePhoneNumber
+ Home telephone number of a user or person.
+"""
+kDSNAttrHomePhoneNumber = "dsAttrTypeStandard:HomePhoneNumber"
+
+"""
+ kDSNAttrHTML
+ HTML location.
+"""
+kDSNAttrHTML = "dsAttrTypeStandard:HTML"
+
+"""
+ kDSNAttrHomeDirectory
+ Network home directory URL.
+"""
+kDSNAttrHomeDirectory = "dsAttrTypeStandard:HomeDirectory"
+
+"""
+ kDSNAttrIMHandle
+ Represents the Instant Messaging handles of a user.
+ Values should be prefixed with the appropriate IM type
+ ie. AIM:, Jabber:, MSN:, Yahoo:, or ICQ:
+ Usually found in user records (kDSStdRecordTypeUsers).
+"""
+kDSNAttrIMHandle = "dsAttrTypeStandard:IMHandle"
+
+"""
+ kDSNAttrIPAddress
+ IP address expressed either as domain or IP notation.
+"""
+kDSNAttrIPAddress = "dsAttrTypeStandard:IPAddress"
+
+"""
+    kDSNAttrIPAddressAndENetAddress
+ A pairing of IPv4 or IPv6 addresses with Ethernet addresses 
+ (e.g., "10.1.1.1/00:16:cb:92:56:41").  Usually found on kDSStdRecordTypeComputers for use by 
+ services that need specific pairing of the two values.  This should be in addition to 
+ kDSNAttrIPAddress, kDSNAttrIPv6Address and kDS1AttrENetAddress. This is necessary because not
+ all directories return attribute values in a guaranteed order.
+"""
+kDSNAttrIPAddressAndENetAddress = "dsAttrTypeStandard:IPAddressAndENetAddress"
+
+"""
+ kDSNAttrIPv6Address
+ IPv6 address expressed in the standard notation (e.g., "fe80::236:caff:fcc2:5641" )
+ Usually found on kDSStdRecordTypeComputers, kDSStdRecordTypeHosts, and 
+ kDSStdRecordTypeMachines.
+"""
+kDSNAttrIPv6Address = "dsAttrTypeStandard:IPv6Address"
+
+"""
+ kDSNAttrJPEGPhoto
+ Used to store binary picture data in JPEG format. 
+ Usually found in user, people or group records (kDSStdRecordTypeUsers, 
+ kDSStdRecordTypePeople, kDSStdRecordTypeGroups).
+"""
+kDSNAttrJPEGPhoto = "dsAttrTypeStandard:JPEGPhoto"
+
+"""
+ kDSNAttrJobTitle
+ Represents the job title of a user.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrJobTitle = "dsAttrTypeStandard:JobTitle"
+
+"""
+ kDSNAttrKDCAuthKey
+ KDC master key RSA encrypted with realm public key.
+"""
+kDSNAttrKDCAuthKey = "dsAttrTypeStandard:KDCAuthKey"
+
+"""
+    kDSNAttrKeywords
+    Keywords using for searching capability.
+"""
+kDSNAttrKeywords = "dsAttrTypeStandard:Keywords"
+
+"""
+ kDSNAttrLDAPReadReplicas
+ List of LDAP server URLs which can each be used to read directory data.
+"""
+kDSNAttrLDAPReadReplicas = "dsAttrTypeStandard:LDAPReadReplicas"
+
+"""
+ kDSNAttrLDAPWriteReplicas
+ List of LDAP server URLs which can each be used to write directory data.
+"""
+kDSNAttrLDAPWriteReplicas = "dsAttrTypeStandard:LDAPWriteReplicas"
+
+"""
+ kDSNAttrMachineServes
+ Attribute type in host or machine records for storing NetInfo 
+        domains served.
+"""
+kDSNAttrMachineServes = "dsAttrTypeStandard:MachineServes"
+
+"""
+ kDSNAttrMapCoordinates
+ attribute that defines coordinates for a user's location .
+*     found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
+    Example: 7.7,10.6
+"""
+kDSNAttrMapCoordinates = "dsAttrTypeStandard:MapCoordinates"
+
+"""
+ kDSNAttrMapURI
+ attribute that defines the URI of a user's location.
+    Usually found in user records (kDSStdRecordTypeUsers). 
+    Example: http://example.com/bldg1
+"""
+kDSNAttrMapURI = "dsAttrTypeStandard:MapURI"
+
+"""
+ kDSNAttrMCXSettings
+ Used by MCX.
+"""
+kDSNAttrMCXSettings = "dsAttrTypeStandard:MCXSettings"
+
+"""
+ kDSNAttrMIME
+ Data contained in this attribute type is a fully qualified MIME Type. 
+"""
+kDSNAttrMIME = "dsAttrTypeStandard:MIME"
+
+"""
+ kDSNAttrMember
+ List of member records. 
+"""
+kDSNAttrMember = "dsAttrTypeStandard:Member"
+
+"""
+ kDSNAttrMobileNumber
+ Represents the mobile numbers of a user or person.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrMobileNumber = "dsAttrTypeStandard:MobileNumber"
+
+"""
+ kDSNAttrNBPEntry
+ Appletalk data.
+"""
+kDSNAttrNBPEntry = "dsAttrTypeStandard:NBPEntry"
+
+"""
+ kDSNAttrNestedGroups
+ Attribute type in group records for the list of GUID values for nested groups.
+"""
+kDSNAttrNestedGroups = "dsAttrTypeStandard:NestedGroups"
+
+"""
+ kDSNAttrNetGroups
+ Attribute type that indicates which netgroups its record is a member of.
+        Found in user, host, and netdomain records.
+"""
+kDSNAttrNetGroups = "dsAttrTypeStandard:NetGroups"
+
+"""
+ kDSNAttrNickName
+ Represents the nickname of a user or person.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrNickName = "dsAttrTypeStandard:NickName"
+
+"""
+ kDSNAttrNodePathXMLPlist
+ Attribute type in Neighborhood records describing the DS Node to search while
+ looking up aliases in this neighborhood.
+"""
+kDSNAttrNodePathXMLPlist = "dsAttrTypeStandard:NodePathXMLPlist"
+
+"""
+ kDSNAttrOrganizationInfo
+ Usually the organization info of a user.
+"""
+kDSNAttrOrganizationInfo = "dsAttrTypeStandard:OrganizationInfo"
+
+"""
+ kDSNAttrOrganizationName
+ Usually the organization of a user.
+"""
+kDSNAttrOrganizationName = "dsAttrTypeStandard:OrganizationName"
+
+"""
+ kDSNAttrPagerNumber
+ Represents the pager numbers of a user or person.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrPagerNumber = "dsAttrTypeStandard:PagerNumber"
+
+"""
+ kDSNAttrPhoneContacts
+ multi-valued attribute that defines a record's custom phone numbers .
+     found in user records (kDSStdRecordTypeUsers). 
+    Example: home fax:408-555-4444
+"""
+kDSNAttrPhoneContacts = "dsAttrTypeStandard:PhoneContacts"
+
+
+"""
+ kDSNAttrPhoneNumber
+ Telephone number of a user.
+"""
+kDSNAttrPhoneNumber = "dsAttrTypeStandard:PhoneNumber"
+
+"""
+ kDSNAttrPGPPublicKey
+ Pretty Good Privacy public encryption key.
+"""
+kDSNAttrPGPPublicKey = "dsAttrTypeStandard:PGPPublicKey"
+
+"""
+ kDSNAttrPostalAddress
+ The postal address usually excluding postal code.
+"""
+kDSNAttrPostalAddress = "dsAttrTypeStandard:PostalAddress"
+
+"""
+* kDSNAttrPostalAddressContacts
+* multi-valued attribute that defines a record's alternate postal addresses .
+*     found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
+"""
+kDSNAttrPostalAddressContacts = "dsAttrTypeStandard:PostalAddressContacts"
+
+"""
+ kDSNAttrPostalCode
+ The postal code such as zip code in the USA.
+"""
+kDSNAttrPostalCode = "dsAttrTypeStandard:PostalCode"
+
+"""
+ kDSNAttrNamePrefix
+ Represents the title prefix of a user or person.
+ ie. Mr., Ms., Mrs., Dr., etc.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrNamePrefix = "dsAttrTypeStandard:NamePrefix"
+
+"""
+ kDSNAttrProtocols
+ List of protocols.
+"""
+kDSNAttrProtocols = "dsAttrTypeStandard:Protocols"
+
+"""
+ kDSNAttrRecordName
+ List of names/keys for this record.
+"""
+kDSNAttrRecordName = "dsAttrTypeStandard:RecordName"
+
+"""
+ kDSNAttrRelationships
+ multi-valued attribute that defines the relationship to the record type .
+     found in user records (kDSStdRecordTypeUsers). 
+    Example: brother:John
+"""
+kDSNAttrRelationships = "dsAttrTypeStandard:Relationships"
+
+"""
+* kDSNAttrResourceInfo
+* multi-valued attribute that defines a resource record's info.
+"""
+kDSNAttrResourceInfo = "dsAttrTypeStandard:ResourceInfo"
+
+"""
+ kDSNAttrResourceType
+ Attribute type for the kind of resource. 
+     found in resource records (kDSStdRecordTypeResources). 
+    Example: ConferenceRoom
+"""
+kDSNAttrResourceType = "dsAttrTypeStandard:ResourceType"
+
+"""
+ kDSNAttrServicesLocator
+ Attribute describing the services hosted for the record.
+"""
+kDSNAttrServicesLocator = "dsAttrTypeStandard:ServicesLocator"
+
+"""
+ kDSNAttrState
+ The state or province of a country.
+"""
+kDSNAttrState = "dsAttrTypeStandard:State"
+
+"""
+ kDSNAttrStreet
+ Represents the street address of a user or person.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrStreet = "dsAttrTypeStandard:Street"
+
+"""
+ kDSNAttrNameSuffix
+ Represents the name suffix of a user or person.
+ ie. Jr., Sr., etc.
+ Usually found in user or people records (kDSStdRecordTypeUsers or 
+ kDSStdRecordTypePeople).
+"""
+kDSNAttrNameSuffix = "dsAttrTypeStandard:NameSuffix"
+
+"""
+ kDSNAttrURL
+ List of URLs.
+"""
+kDSNAttrURL = "dsAttrTypeStandard:URL"
+
+"""
+ kDSNAttrURLForNSL
+ List of URLs used by NSL.
+"""
+kDSNAttrURLForNSL = "dsAttrTypeStandard:URLForNSL"
+
+"""
+ kDSNAttrVFSOpts
+ Used in support of mount records.
+"""
+kDSNAttrVFSOpts = "dsAttrTypeStandard:VFSOpts"
+
+
+
+# Other Attribute Type Constants
+
+
+"""
+ DirectoryService Other Attribute Type Constants Not Mapped by Directory Node Plugins
+ Mainly used internally by the DirectoryService Daemon or made available via dsGetDirNodeInfo()
+"""
+
+"""
+ kDS1AttrAdminStatus
+ Retained only for backward compatibility.
+"""
+kDS1AttrAdminStatus = "dsAttrTypeStandard:AdminStatus"
+
+"""
+ kDS1AttrAlias
+ Alias attribute, contain pointer to another node/record/attribute.
+"""
+kDS1AttrAlias = "dsAttrTypeStandard:Alias"
+
+"""
+ kDS1AttrAuthCredential
+ An "auth" credential, to be used to authenticate to other Directory nodes.
+"""
+kDS1AttrAuthCredential = "dsAttrTypeStandard:AuthCredential"
+
+"""
+ kDS1AttrCopyTimestamp
+ Timestamp used in local account caching.
+"""
+kDS1AttrCopyTimestamp = "dsAttrTypeStandard:CopyTimestamp"
+
+"""
+    kDS1AttrDateRecordCreated
+    Date of record creation.
+"""
+kDS1AttrDateRecordCreated = "dsAttrTypeStandard:DateRecordCreated"
+
+"""
+ kDS1AttrKerberosRealm
+ Supports Kerberized SMB Server services.
+"""
+kDS1AttrKerberosRealm = "dsAttrTypeStandard:KerberosRealm"
+
+"""
+ kDS1AttrNTDomainComputerAccount
+ Supports Kerberized SMB Server services.
+"""
+kDS1AttrNTDomainComputerAccount = "dsAttrTypeStandard:NTDomainComputerAccount"
+
+"""
+ kDSNAttrOriginalHomeDirectory
+ Home directory URL used in local account caching.
+"""
+kDSNAttrOriginalHomeDirectory = "dsAttrTypeStandard:OriginalHomeDirectory"
+
+"""
+ kDS1AttrOriginalNFSHomeDirectory
+ NFS home directory used in local account caching.
+"""
+kDS1AttrOriginalNFSHomeDirectory = "dsAttrTypeStandard:OriginalNFSHomeDirectory"
+
+"""
+ kDS1AttrOriginalNodeName
+ Nodename used in local account caching.
+"""
+kDS1AttrOriginalNodeName = "dsAttrTypeStandard:OriginalNodeName"
+
+"""
+ kDS1AttrPrimaryNTDomain
+ Supports Kerberized SMB Server services.
+"""
+kDS1AttrPrimaryNTDomain = "dsAttrTypeStandard:PrimaryNTDomain"
+
+"""
+ kDS1AttrPwdAgingPolicy
+ Contains the password aging policy data for an authentication capable record.
+"""
+kDS1AttrPwdAgingPolicy = "dsAttrTypeStandard:PwdAgingPolicy"
+
+"""
+ kDS1AttrRARA
+ Retained only for backward compatibility.
+"""
+kDS1AttrRARA = "dsAttrTypeStandard:RARA"
+
+"""
+ kDS1AttrReadOnlyNode
+ Can be found using dsGetDirNodeInfo and will return one of
+ ReadOnly, ReadWrite, or WriteOnly strings.
+ Note that ReadWrite does not imply fully readable or writable
+"""
+kDS1AttrReadOnlyNode = "dsAttrTypeStandard:ReadOnlyNode"
+
+"""
+ kDS1AttrRecordImage
+ A binary image of the record and all it's attributes.
+ Has never been supported.
+"""
+kDS1AttrRecordImage = "dsAttrTypeStandard:RecordImage"
+
+"""
+ kDS1AttrSMBGroupRID
+ Attributefor supporting PDC SMB interaction.
+"""
+kDS1AttrSMBGroupRID = "dsAttrTypeStandard:SMBGroupRID"
+
+"""
+ kDS1AttrTimePackage
+ Data of Create, Modify, Backup time in UTC.
+"""
+kDS1AttrTimePackage = "dsAttrTypeStandard:TimePackage"
+
+"""
+ kDS1AttrTotalSize
+ checksum/meta data.
+"""
+kDS1AttrTotalSize = "dsAttrTypeStandard:TotalSize"
+
+"""
+ kDSNAttrAllNames
+ Backward compatibility only - all possible names for a record.
+ Has never been supported.
+"""
+kDSNAttrAllNames = "dsAttrTypeStandard:AllNames"
+
+"""
+ kDSNAttrAuthMethod
+ Authentication method for an authentication capable record.
+"""
+kDSNAttrAuthMethod = "dsAttrTypeStandard:AuthMethod"
+
+"""
+ kDSNAttrMetaNodeLocation
+ Meta attribute returning registered node name by directory node plugin.
+"""
+kDSNAttrMetaNodeLocation = "dsAttrTypeStandard:AppleMetaNodeLocation"
+
+"""
+ kDSNAttrNodePath
+ Sub strings of a Directory Service Node given in order.
+"""
+kDSNAttrNodePath = "dsAttrTypeStandard:NodePath"
+
+"""
+ kDSNAttrPlugInInfo
+ Information (version, signature, about, credits, etc.) about the plug-in
+ that is actually servicing a particular directory node.
+ Has never been supported.
+"""
+kDSNAttrPlugInInfo = "dsAttrTypeStandard:PlugInInfo"
+
+"""
+ kDSNAttrRecordAlias
+ No longer supported in Mac OS X 10.4 or later.
+"""
+kDSNAttrRecordAlias = "dsAttrTypeStandard:RecordAlias"
+
+"""
+ kDSNAttrRecordType
+ Single Valued for a Record, Multi-valued for a Directory Node.
+"""
+kDSNAttrRecordType = "dsAttrTypeStandard:RecordType"
+
+"""
+ kDSNAttrSchema
+ List of attribute types.
+"""
+kDSNAttrSchema = "dsAttrTypeStandard:Scheama"
+
+"""
+ kDSNAttrSetPasswdMethod
+ Retained only for backward compatibility.
+"""
+kDSNAttrSetPasswdMethod = "dsAttrTypeStandard:SetPasswdMethod"
+
+"""
+ kDSNAttrSubNodes
+ Attribute of a node which lists the available subnodes
+        of that node.
+"""
+kDSNAttrSubNodes = "dsAttrTypeStandard:SubNodes"
+
+"""
+ kStandardSourceAlias
+ No longer supported in Mac OS X 10.4 or later.
+"""
+kStandardSourceAlias = "dsAttrTypeStandard:AppleMetaAliasSource"
+
+"""
+ kStandardTargetAlias
+ No longer supported in Mac OS X 10.4 or later.
+"""
+kStandardTargetAlias = "dsAttrTypeStandard:AppleMetaAliasTarget"
+
+"""
+ kDSNAttrNetGroupTriplet
+ Multivalued attribute that defines the host, user and domain triplet combinations
+  to support NetGroups.  Each attribute value is comma separated string to maintain the
+  triplet (e.g., host,user,domain).
+"""
+kDSNAttrNetGroupTriplet = "dsAttrTypeStandard:NetGroupTriplet"
+
+
+# Search Node attribute type Constants
+
+
+"""
+ Search Node attribute type Constants
+"""
+ 
+"""
+ kDS1AttrSearchPath
+ Search path used by the search node.
+"""
+kDS1AttrSearchPath = "dsAttrTypeStandard:SearchPath"
+
+"""
+ kDSNAttrSearchPath
+ Retained only for backward compatibility.
+"""
+kDSNAttrSearchPath = "dsAttrTypeStandard:SearchPath"
+
+"""
+ kDS1AttrSearchPolicy
+ Search policy for the search node.
+"""
+kDS1AttrSearchPolicy = "dsAttrTypeStandard:SearchPolicy"
+
+"""
+ kDS1AttrNSPSearchPath
+ Automatic search path defined by the search node.
+"""
+kDS1AttrNSPSearchPath = "dsAttrTypeStandard:NSPSearchPath"
+
+"""
+ kDSNAttrNSPSearchPath
+ Retained only for backward compatibility.
+"""
+kDSNAttrNSPSearchPath = "dsAttrTypeStandard:NSPSearchPath"
+
+"""
+ kDS1AttrLSPSearchPath
+ Local only search path defined by the search node.
+"""
+kDS1AttrLSPSearchPath = "dsAttrTypeStandard:LSPSearchPath"
+
+"""
+ kDSNAttrLSPSearchPath
+ Retained only for backward compatibility.
+"""
+kDSNAttrLSPSearchPath = "dsAttrTypeStandard:LSPSearchPath"
+
+"""
+ kDS1AttrCSPSearchPath
+ Admin user configured custom search path defined by the search node.
+"""
+kDS1AttrCSPSearchPath = "dsAttrTypeStandard:CSPSearchPath"
+
+"""
+ kDSNAttrCSPSearchPath
+ Retained only for backward compatibility.
+"""
+kDSNAttrCSPSearchPath = "dsAttrTypeStandard:CSPSearchPath"
+

Added: CalendarServer/trunk/calendarserver/od/dsquery.py
===================================================================
--- CalendarServer/trunk/calendarserver/od/dsquery.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/od/dsquery.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -0,0 +1,134 @@
+##
+# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+##
+
+"""
+Compound query builder. We do this in Python to avoid having to mess
+with pass a complex Python object hierarchy into C. These classes allow us to
+build the query in Python and generate the compound query string that the directory
+service C api requires.
+"""
+
+import dsattributes
+
+class match(object):
+    """
+    Represents and attribute/value match operation.
+    """
+
+    def __init__(self, attribute, value, matchType):
+        self.attribute = attribute
+        self.value = value
+        self.matchType = matchType
+
+    def generate(self):
+        return {
+            dsattributes.eDSExact :        "(%s=%s)",
+            dsattributes.eDSStartsWith :   "(%s=%s*)",
+            dsattributes.eDSEndsWith :     "(%s=*%s)",
+            dsattributes.eDSContains :     "(%s=*%s*)",
+            dsattributes.eDSLessThan :     "(%s<%s)",
+            dsattributes.eDSGreaterThan :  "(%s>%s)",
+        }.get(self.matchType, "(%s=*%s*)") % (self.attribute, self.value,)
+
+class expression(object):
+    """
+    Represents a query expression that includes a boolean operator, and a list
+    of sub-expressions operated on. The sub-expressions can either be another expression
+    object or a match object.
+    """
+
+    AND = "&"
+    OR  = "|"
+    NOT = "!"
+
+    def __init__(self, operator, subexpressions):
+        assert(operator == expression.AND or operator == expression.OR or operator == expression.NOT)
+        self.operator = operator
+        self.subexpressions = subexpressions
+
+    def generate(self):
+        result = ""
+        if self.operator == expression.NOT:
+            result += "("
+            result += self.operator
+            result += self.subexpressions.generate()
+            result += ")"
+        else:
+            if len(self.subexpressions) > 1:
+                result += "("
+                result += self.operator
+            for sub in self.subexpressions:
+                result += sub.generate()
+            if len(self.subexpressions) > 1:
+                result += ")"
+        return result
+
+
+# Do some tests
+if __name__=='__main__':
+    exprs = (
+        (expression(
+            expression.AND, (
+                expression(expression.OR, (match("ResourceType", "xyz", dsattributes.eDSExact), match("ResourceType", "abc", dsattributes.eDSExact))),
+                match("ServicesLocator", "GUID:VGUID:calendar", dsattributes.eDSStartsWith),
+            )
+        ), "(&(|(ResourceType=xyz)(ResourceType=abc))(ServicesLocator=GUID:VGUID:calendar*))"),
+        (expression(
+            expression.AND, (
+                expression(expression.OR, (match("ResourceType", "xyz", dsattributes.eDSStartsWith), match("ResourceType", "abc", dsattributes.eDSEndsWith))),
+                match("ServicesLocator", "GUID:VGUID:calendar", dsattributes.eDSContains),
+            )
+        ), "(&(|(ResourceType=xyz*)(ResourceType=*abc))(ServicesLocator=*GUID:VGUID:calendar*))"),
+        (expression(
+            expression.AND, (
+                expression(expression.AND, (match("ResourceType", "xyz", dsattributes.eDSLessThan), match("ResourceType", "abc", dsattributes.eDSGreaterThan))),
+                match("ServicesLocator", "GUID:VGUID:calendar", 0xBAD),
+            )
+        ), "(&(&(ResourceType<xyz)(ResourceType>abc))(ServicesLocator=*GUID:VGUID:calendar*))"),
+        (expression(
+            expression.AND, (
+                match("ServicesLocator", "GUID:VGUID:calendar", 0xBAD),
+            )
+        ), "(ServicesLocator=*GUID:VGUID:calendar*)"),
+        (expression(
+            expression.NOT, match(dsattributes.kDSNAttrNickName, "", dsattributes.eDSStartsWith )
+        ), "(!(" + dsattributes.kDSNAttrNickName + "=*))"),
+        (expression(
+            expression.AND, (
+               expression(
+                    expression.NOT, match(dsattributes.kDSNAttrNickName, "Billy", dsattributes.eDSContains )
+               ),
+               expression(
+                    expression.NOT, match(dsattributes.kDSNAttrEMailAddress, "Billy", dsattributes.eDSContains )
+               ),
+            ),
+        ), "(&(!(" + dsattributes.kDSNAttrNickName + "=*Billy*))(!(" + dsattributes.kDSNAttrEMailAddress + "=*Billy*)))"),
+        (expression(
+            expression.NOT, expression(
+                    expression.OR, (
+                        match(dsattributes.kDSNAttrNickName, "", dsattributes.eDSStartsWith ),
+                        match(dsattributes.kDSNAttrEMailAddress, "", dsattributes.eDSStartsWith ),
+                    ),
+            ),
+        ), "(!(|("+dsattributes.kDSNAttrNickName+"=*)("+dsattributes.kDSNAttrEMailAddress+"=*)))"),
+    )
+
+    for expr, result in exprs:
+        gen = expr.generate()
+        if gen != result:
+            print "Generate expression %s != %s" % (gen, result,)
+    print "Done."

Added: CalendarServer/trunk/calendarserver/od/opendirectory.py
===================================================================
--- CalendarServer/trunk/calendarserver/od/opendirectory.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/od/opendirectory.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -0,0 +1,319 @@
+##
+# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+OpenDirectory.framework access via PyObjC
+"""
+
+import OpenDirectory
+from twext.python.log import Logger
+
+log = Logger()
+
+# Single-value attributes (must be converted from lists):
+SINGLE_VALUE_ATTRIBUTES = [
+    OpenDirectory.kODAttributeTypeBirthday,
+    OpenDirectory.kODAttributeTypeComment,
+    OpenDirectory.kODAttributeTypeCreationTimestamp,
+    OpenDirectory.kODAttributeTypeFullName,
+    OpenDirectory.kODAttributeTypeFirstName,
+    OpenDirectory.kODAttributeTypeGUID,
+    OpenDirectory.kODAttributeTypeLastName,
+    OpenDirectory.kODAttributeTypeMiddleName,
+    OpenDirectory.kODAttributeTypeModificationTimestamp,
+    OpenDirectory.kODAttributeTypeNote,
+    OpenDirectory.kODAttributeTypeSearchPath,
+    OpenDirectory.kODAttributeTypeUserCertificate,
+    OpenDirectory.kODAttributeTypeUserPKCS12Data,
+    OpenDirectory.kODAttributeTypeUserSMIMECertificate,
+    OpenDirectory.kODAttributeTypeWeblogURI,
+]
+
+
+class Directory(object):
+    """ Encapsulates OpenDirectory session and node """
+
+    def __init__(self, session, node, nodeName):
+        self.session = session
+        self.node = node
+        self.nodeName = nodeName
+
+    def __str__(self):
+        return "OpenDirectory node: %s" % (self.nodeName)
+
+
+caseInsensitiveEquivalents = {
+    OpenDirectory.kODMatchBeginsWith : OpenDirectory.kODMatchInsensitiveBeginsWith,
+    OpenDirectory.kODMatchContains : OpenDirectory.kODMatchInsensitiveContains,
+    OpenDirectory.kODMatchEndsWith : OpenDirectory.kODMatchInsensitiveEndsWith,
+    OpenDirectory.kODMatchEqualTo : OpenDirectory.kODMatchInsensitiveEqualTo,
+}
+
+def adjustMatchType(matchType, caseInsensitive):
+    """ Return the case-insensitive equivalent matchType """
+    return caseInsensitiveEquivalents[matchType] if caseInsensitive else matchType
+
+def recordToResult(record):
+    details, error = record.recordDetailsForAttributes_error_(None, None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+    result = {}
+    for key, value in details.iteritems():
+        if key in SINGLE_VALUE_ATTRIBUTES:
+            result[key] = None if len(value) == 0 else unicode(value[0])
+        else:
+            result[key] = [unicode(v) for v in value]
+
+    return (details.get(OpenDirectory.kODAttributeTypeRecordName, [None])[0], result)
+
+def odInit(nodeName):
+    """
+    Create an Open Directory object to operate on the specified directory service node name.
+
+    @param nodeName: C{str} containing the node name.
+    @return: C{object} an object to be passed to all subsequent functions on success,
+        C{None} on failure.
+    """
+    session = OpenDirectory.ODSession.defaultSession()
+    node, error = OpenDirectory.ODNode.nodeWithSession_name_error_(session,
+        nodeName, None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+
+    return Directory(session, node, nodeName)
+
+
+def getNodeAttributes(directory, nodeName, attributes):
+    """
+    Return key attributes for the specified directory node. The attributes
+    can be a C{str} for the attribute name, or a C{tuple} or C{list} where the first C{str}
+    is the attribute name, and the second C{str} is an encoding type, either "str" or "base64".
+
+    @param directory: C{Directory} the object obtained from an odInit call.
+    @param nodeName: C{str} containing the OD nodeName to query.
+    @param attributes: C{list} or C{tuple} containing the attributes to return for each record.
+    @return: C{dict} of attributes found.
+    """
+    details, error = directory.node.nodeDetailsForKeys_error_(attributes, None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+    return details
+
+
+def listAllRecordsWithAttributes_list(directory, recordType, attributes, count=0):
+    """
+    List records in Open Directory, and return key attributes for each one.
+    The attributes can be a C{str} for the attribute name, or a C{tuple} or C{list} where the first C{str}
+    is the attribute name, and the second C{str} is an encoding type, either "str" or "base64".
+
+    @param directory: C{Directory} the object obtained from an odInit call.
+    @param recordType: C{str}, C{tuple} or C{list} containing the OD record types to lookup.
+    @param attributes: C{list} or C{tuple} containing the attributes to return for each record.
+    @param count: C{int} maximum number of records to return (zero returns all).
+    @return: C{list} containing a C{list} of C{str} (record name) and C{dict} attributes
+        for each record found, or C{None} otherwise.
+    """
+    query, error = OpenDirectory.ODQuery.queryWithNode_forRecordTypes_attribute_matchType_queryValues_returnAttributes_maximumResults_error_(
+        directory.node,
+        recordType,
+        None,
+        OpenDirectory.kODMatchAny,
+        None,
+        attributes,
+        count,
+        None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+    records, error = query.resultsAllowingPartial_error_(False, None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+    for record in records:
+        yield recordToResult(record)
+
+def queryRecordsWithAttribute_list(directory, attr, value, matchType, casei, recordType, attributes, count=0):
+    """
+    List records in Open Directory matching specified attribute/value, and return key attributes for each one.
+    The attributes can be a C{str} for the attribute name, or a C{tuple} or C{list} where the first C{str}
+    is the attribute name, and the second C{str} is an encoding type, either "str" or "base64".
+
+    @param directory: C{Directory} the object obtained from an odInit call.
+    @param attr: C{str} containing the attribute to search.
+    @param value: C{str} containing the value to search for.
+    @param matchType: C{int} DS match type to use when searching.
+    @param casei: C{True} to do case-insensitive match, C{False} otherwise.
+    @param recordType: C{str}, C{tuple} or C{list} containing the OD record types to lookup.
+    @param attributes: C{list} or C{tuple} containing the attributes to return for each record.
+    @param count: C{int} maximum number of records to return (zero returns all).
+    @return: C{list} containing a C{list} of C{str} (record name) and C{dict} attributes
+        for each record found, or C{None} otherwise.
+    """
+
+    query, error = OpenDirectory.ODQuery.queryWithNode_forRecordTypes_attribute_matchType_queryValues_returnAttributes_maximumResults_error_(
+        directory.node,
+        recordType,
+        attr,
+        adjustMatchType(matchType, casei),
+        value,
+        attributes,
+        count,
+        None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+    records, error = query.resultsAllowingPartial_error_(False, None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+    for record in records:
+        yield recordToResult(record)
+
+
+def queryRecordsWithAttributes_list(directory, compound, casei, recordType, attributes, count=0):
+    """
+    List records in Open Directory matching specified criteria, and return key attributes for each one.
+    The attributes can be a C{str} for the attribute name, or a C{tuple} or C{list} where the first C{str}
+    is the attribute name, and the second C{str} is an encoding type, either "str" or "base64".
+
+    @param directory: C{Directory} the object obtained from an odInit call.
+    @param compound: C{str} containing the compound search query to use.
+    @param casei: C{True} to do case-insensitive match, C{False} otherwise.
+    @param recordType: C{str}, C{tuple} or C{list} containing the OD record types to lookup.
+    @param attributes: C{list} or C{tuple} containing the attributes to return for each record.
+    @param count: C{int} maximum number of records to return (zero returns all).
+    @return: C{list} containing a C{list} of C{str} (record name) and C{dict} attributes
+        for each record found, or C{None} otherwise.
+    """
+    query, error = OpenDirectory.ODQuery.queryWithNode_forRecordTypes_attribute_matchType_queryValues_returnAttributes_maximumResults_error_(
+        directory.node,
+        recordType,
+        None,
+        0x210B, # adjustMatchType(matchType, casei),
+        compound,
+        attributes,
+        count,
+        None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+    records, error = query.resultsAllowingPartial_error_(False, None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+    for record in records:
+        yield recordToResult(record)
+
+
+def getUserRecord(directory, user):
+    """
+    Look up the record for the given user within the directory's node
+
+    @param directory: C{Directory} the object obtained from an odInit call.
+    @param user: C{str} the user identifier/directory record name to fetch.
+    @return: OD record if the user was found, None otherwise.
+    """
+    record, error = directory.node.recordWithRecordType_name_attributes_error_(
+        OpenDirectory.kODRecordTypeUsers,
+        user,
+        None,
+        None
+    )
+    if error:
+        log.error(error)
+        raise ODError(error)
+    return record
+
+def authenticateUserBasic(directory, nodeName, user, password):
+    """
+    Authenticate a user with a password to Open Directory.
+
+    @param directory: C{Directory} the object obtained from an odInit call.
+    @param nodeName: C{str} the directory nodeName for the record to check.
+    @param user: C{str} the user identifier/directory record name to check.
+    @param pswd: C{str} containing the password to check.
+    @return: C{True} if the user was found, C{False} otherwise.
+    """
+    record = getUserRecord(directory, user)
+    if record is None:
+        raise ODError("Record not found")
+
+    result, error = record.verifyPassword_error_(password, None)
+    if error:
+        log.error(error)
+        raise ODError(error)
+    return result
+
+
+def authenticateUserDigest(directory, nodeName, user, challenge, response, method):
+    """
+    Authenticate using HTTP Digest credentials to Open Directory.
+
+    @param directory: C{Directory} the object obtained from an odInit call.
+    @param nodeName: C{str} the directory nodeName for the record to check.
+    @param user: C{str} the user identifier/directory record name to check.
+    @param challenge: C{str} the HTTP challenge sent to the client.
+    @param response: C{str} the HTTP response sent from the client.
+    @param method: C{str} the HTTP method being used.
+    @return: C{True} if the user was found, C{False} otherwise.
+    """
+    record = getUserRecord(directory, user)
+    if record is None:
+        raise ODError("Record not found")
+
+    # TODO: what are these other return values?
+    result, mystery1, mystery2, error = record.verifyExtendedWithAuthenticationType_authenticationItems_continueItems_context_error_(
+        OpenDirectory.kODAuthenticationTypeDIGEST_MD5,
+        [user, challenge, response, method],
+        None, None, None
+    )
+    if error:
+        log.error(error)
+        raise ODError(error)
+    return result
+
+
+
+def authenticateUserDigestToActiveDirectory(obj, nodeName, user, response):
+    """
+    Authenticate using HTTP Digest credentials to an Active Directory node, 
+    exported by Open Diretory
+
+    @param obj: C{object} the object obtained from an odInit call.
+    @param nodeName: C{str} the directory nodeName for the record to check.
+    @param user: C{str} the user identifier/directory record name to check.
+    @param response: C{str} the HTTP response sent from the client.
+    @return: C{True} if the user was found, C{False} otherwise.
+    """
+
+def getDigestMD5ChallengeFromActiveDirectory(obj, nodeName):
+    """
+    Get an DigestMD5 challenge from Active Directory
+
+    @param obj: C{object} the object obtained from an odInit call.
+    @param nodeName: C{str} the directory nodeName for the record to check.
+    @return: C{string} containing the challenge
+    """
+
+class ODError(Exception):
+    """
+    Exceptions from DirectoryServices errors.
+    """
+    def __init__(self, error):
+        self.message = (str(error), error.code())

Added: CalendarServer/trunk/calendarserver/od/test/__init__.py
===================================================================
--- CalendarServer/trunk/calendarserver/od/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/od/test/__init__.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: CalendarServer/trunk/calendarserver/od/test/setup_directory.py
===================================================================
--- CalendarServer/trunk/calendarserver/od/test/setup_directory.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/od/test/setup_directory.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -0,0 +1,292 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import os
+import sys
+import OpenDirectory
+from getopt import getopt, GetoptError
+
+# TODO: Nested groups
+# TODO: GroupMembership
+
+masterNodeName = "/LDAPv3/127.0.0.1"
+localNodeName = "/Local/Default"
+
+masterUsers = [
+    (
+        "odtestamanda",
+        {
+            OpenDirectory.kODAttributeTypePassword : ["password"],
+            OpenDirectory.kODAttributeTypeFirstName : ["Amanda"],
+            OpenDirectory.kODAttributeTypeLastName  : ["Test"],
+            OpenDirectory.kODAttributeTypeFullName : ["Amanda Test"],
+            OpenDirectory.kODAttributeTypeEMailAddress : ["amanda at example.com"],
+            OpenDirectory.kODAttributeTypeGUID : ["9dc04a70-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeUniqueID : ["33300"],
+        },
+    ),
+    (
+        "odtestbetty",
+        {
+            OpenDirectory.kODAttributeTypePassword : ["password"],
+            OpenDirectory.kODAttributeTypeFirstName : ["Betty"],
+            OpenDirectory.kODAttributeTypeLastName  : ["Test"],
+            OpenDirectory.kODAttributeTypeFullName : ["Betty Test"],
+            OpenDirectory.kODAttributeTypeEMailAddress : ["betty at example.com"],
+            OpenDirectory.kODAttributeTypeGUID : ["9dc04a71-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeUniqueID : ["33301"],
+        },
+    ),
+    (
+        "odtestcarlene",
+        {
+            OpenDirectory.kODAttributeTypePassword : ["password"],
+            OpenDirectory.kODAttributeTypeFirstName : ["Carlene"],
+            OpenDirectory.kODAttributeTypeLastName  : ["Test"],
+            OpenDirectory.kODAttributeTypeEMailAddress : ["carlene at example.com"],
+            OpenDirectory.kODAttributeTypeGUID : ["9dc04a72-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeUniqueID : ["33302"],
+        },
+    ),
+    (
+        "odtestdenise",
+        {
+            OpenDirectory.kODAttributeTypePassword : ["password"],
+            OpenDirectory.kODAttributeTypeFirstName : ["Denise"],
+            OpenDirectory.kODAttributeTypeLastName  : ["Test"],
+            OpenDirectory.kODAttributeTypeEMailAddress : ["denise at example.com"],
+            OpenDirectory.kODAttributeTypeGUID : ["9dc04a73-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeUniqueID : ["33303"],
+        },
+    ),
+]
+
+masterGroups = [
+    (
+        "odtestgrouptop",
+        {
+            OpenDirectory.kODAttributeTypeGUID : ["6c6cd280-e6e3-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeFullName : ["OD Test Group Top"],
+            OpenDirectory.kODAttributeTypeGroupMembers : ["9dc04a70-e6dd-11df-9492-0800200c9a66", "9dc04a71-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypePrimaryGroupID : ["33400"],
+        },
+    ),
+]
+
+localUsers = [
+    (
+        "odtestalbert",
+        {
+            OpenDirectory.kODAttributeTypePassword : ["password"],
+            OpenDirectory.kODAttributeTypeFirstName : ["Albert"],
+            OpenDirectory.kODAttributeTypeLastName  : ["Test"],
+            OpenDirectory.kODAttributeTypeFullName : ["Albert Test"],
+            OpenDirectory.kODAttributeTypeEMailAddress : ["albert at example.com"],
+            OpenDirectory.kODAttributeTypeGUID : ["9dc04a74-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeUniqueID : ["33350"],
+        },
+    ),
+    (
+        "odtestbill",
+        {
+            OpenDirectory.kODAttributeTypePassword : ["password"],
+            OpenDirectory.kODAttributeTypeFirstName : ["Bill"],
+            OpenDirectory.kODAttributeTypeLastName  : ["Test"],
+            OpenDirectory.kODAttributeTypeFullName : ["Bill Test"],
+            OpenDirectory.kODAttributeTypeEMailAddress : ["bill at example.com"],
+            OpenDirectory.kODAttributeTypeGUID : ["9dc04a75-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeUniqueID : ["33351"],
+        },
+    ),
+    (
+        "odtestcarl",
+        {
+            OpenDirectory.kODAttributeTypePassword : ["password"],
+            OpenDirectory.kODAttributeTypeFirstName : ["Carl"],
+            OpenDirectory.kODAttributeTypeLastName  : ["Test"],
+            OpenDirectory.kODAttributeTypeEMailAddress : ["carl at example.com"],
+            OpenDirectory.kODAttributeTypeGUID : ["9dc04a76-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeUniqueID : ["33352"],
+        },
+    ),
+    (
+        "odtestdavid",
+        {
+            OpenDirectory.kODAttributeTypePassword : ["password"],
+            OpenDirectory.kODAttributeTypeFirstName : ["David"],
+            OpenDirectory.kODAttributeTypeLastName  : ["Test"],
+            OpenDirectory.kODAttributeTypeEMailAddress : ["david at example.com"],
+            OpenDirectory.kODAttributeTypeGUID : ["9dc04a77-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeUniqueID : ["33353"],
+        },
+    ),
+]
+
+localGroups = [
+    (
+        "odtestsubgroupa",
+        {
+            OpenDirectory.kODAttributeTypeGUID : ["6c6cd281-e6e3-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypeFullName : ["OD Test Subgroup A"],
+            OpenDirectory.kODAttributeTypeGroupMembers : ["9dc04a74-e6dd-11df-9492-0800200c9a66", "9dc04a75-e6dd-11df-9492-0800200c9a66"],
+            OpenDirectory.kODAttributeTypePrimaryGroupID : ["33400"],
+        },
+    ),
+]
+
+
+def usage(e=None):
+    name = os.path.basename(sys.argv[0])
+    print "usage: %s [options] local_user local_password odmaster_user odmaster_password" % (name,)
+    print ""
+    print " Configures local and OD master directories for testing"
+    print ""
+    print "options:"
+    print " -h --help: print this help and exit"
+    if e:
+        sys.exit(1)
+    else:
+        sys.exit(0)
+
+
+def lookupRecordName(node, recordType, name):
+    query, error = OpenDirectory.ODQuery.queryWithNode_forRecordTypes_attribute_matchType_queryValues_returnAttributes_maximumResults_error_(
+        node,
+        recordType,
+        OpenDirectory.kODAttributeTypeRecordName,
+        OpenDirectory.kODMatchEqualTo,
+        name,
+        None,
+        0,
+        None)
+    if error:
+        raise ODError(error)
+    records, error = query.resultsAllowingPartial_error_(False, None)
+    if error:
+        raise ODError(error)
+
+    if len(records) < 1:
+        return None
+    if len(records) > 1:
+        raise ODError("Multiple records for '%s' were found" % (name,))
+
+    return records[0]
+
+def createRecord(node, recordType, recordName, attrs):
+    record, error = node.createRecordWithRecordType_name_attributes_error_(
+        recordType,
+        recordName,
+        attrs,
+        None)
+    if error:
+        print error
+        raise ODError(error)
+    return record
+
+def main():
+
+    try:
+        (optargs, args) = getopt(sys.argv[1:], "h", ["help"])
+    except GetoptError, e:
+        usage(e)
+
+    for opt, arg in optargs:
+        if opt in ("-h", "--help"):
+            usage()
+
+    if len(args) != 4:
+        usage()
+
+    localUser, localPassword, masterUser, masterPassword = args
+
+    userInfo = {
+        masterNodeName : {
+            "user" : masterUser,
+            "password" : masterPassword,
+            "users" : masterUsers,
+            "groups" : masterGroups,
+        },
+        localNodeName : {
+            "user" : localUser,
+            "password" : localPassword,
+            "users" : localUsers,
+            "groups" : localGroups,
+        },
+    }
+
+
+    session = OpenDirectory.ODSession.defaultSession()
+
+    for nodeName, info in userInfo.iteritems():
+
+        userName = info["user"]
+        password = info["password"]
+        users = info["users"]
+        groups = info["groups"]
+
+        node, error = OpenDirectory.ODNode.nodeWithSession_name_error_(session, nodeName, None)
+        if error:
+            print error
+            raise ODError(error)
+
+        result, error = node.setCredentialsWithRecordType_recordName_password_error_(
+            OpenDirectory.kODRecordTypeUsers,
+            userName,
+            password,
+            None
+        )
+        if error:
+            print "Unable to authenticate with directory %s: %s" % (nodeName, error)
+            raise ODError(error)
+
+        print "Successfully authenticated with directory %s" % (nodeName,)
+
+        print "Creating users within %s:" % (nodeName,)
+        for recordName, attrs in users:
+            record = lookupRecordName(node, OpenDirectory.kODRecordTypeUsers, recordName)
+            if record is None:
+                print "Creating user %s" % (recordName,)
+                try:
+                    record = createRecord(node, OpenDirectory.kODRecordTypeUsers, recordName, attrs)
+                    print "Successfully created user %s" % (recordName,)
+                except ODError, e:
+                    print "Failed to create user %s: %s" % (recordName, e)
+            else:
+                print "User %s already exists" % (recordName,)
+
+        print "Creating groups within %s:" % (nodeName,)
+        for recordName, attrs in groups:
+            record = lookupRecordName(node, OpenDirectory.kODRecordTypeGroups, recordName)
+            if record is None:
+                print "Creating group %s" % (recordName,)
+                try:
+                    record = createRecord(node, OpenDirectory.kODRecordTypeGroups, recordName, attrs)
+                    print "Successfully created group %s" % (recordName,)
+                except ODError, e:
+                    print "Failed to create group %s: %s" % (recordName, e)
+            else:
+                print "Group %s already exists" % (recordName,)
+
+        print
+
+
+
+class ODError(Exception):
+    def __init__(self, error):
+        self.message = (str(error), error.code())
+
+if __name__ == "__main__":
+    main()

Added: CalendarServer/trunk/calendarserver/od/test/test_opendirectory.py
===================================================================
--- CalendarServer/trunk/calendarserver/od/test/test_opendirectory.py	                        (rev 0)
+++ CalendarServer/trunk/calendarserver/od/test/test_opendirectory.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -0,0 +1,733 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.test.util import TestCase
+
+runTests = False
+
+try:
+    import OpenDirectory
+    from calendarserver.od import opendirectory, dsattributes, dsquery
+    import setup_directory
+
+    directory = opendirectory.odInit("/Search")
+
+    results = list(opendirectory.queryRecordsWithAttribute_list(
+        directory,
+        dsattributes.kDS1AttrGeneratedUID,
+        "9dc04a74-e6dd-11df-9492-0800200c9a66",
+        dsattributes.eDSExact,
+        False,
+        dsattributes.kDSStdRecordTypeUsers,
+        None,
+        count=0
+    ))
+    recordNames = [x[0] for x in results]
+    # Local user:
+    if "odtestalbert" in recordNames:
+        runTests = True
+    else:
+        print "Please run setup_directory.py to populate OD"
+
+except ImportError:
+    print "Unable to import OpenDirectory framework"
+
+if runTests:
+
+    USER_ATTRIBUTES = [
+        dsattributes.kDS1AttrGeneratedUID,
+        dsattributes.kDSNAttrRecordName,
+        dsattributes.kDSNAttrAltSecurityIdentities,
+        dsattributes.kDSNAttrRecordType,
+        dsattributes.kDS1AttrDistinguishedName,
+        dsattributes.kDS1AttrFirstName,
+        dsattributes.kDS1AttrLastName,
+        dsattributes.kDSNAttrEMailAddress,
+        dsattributes.kDSNAttrMetaNodeLocation,
+    ]
+
+    class OpenDirectoryTests(TestCase):
+
+        def test_odInit(self):
+
+            # Bogus node name
+            self.assertRaises(opendirectory.ODError, opendirectory.odInit, "/Foo")
+
+            # Valid node name
+            directory = opendirectory.odInit("/Search")
+            self.assertTrue(isinstance(directory, opendirectory.Directory))
+
+        def test_adjustMatchType(self):
+            self.assertEquals(
+                opendirectory.adjustMatchType(OpenDirectory.kODMatchEqualTo, False),
+                OpenDirectory.kODMatchEqualTo
+            )
+            self.assertEquals(
+                opendirectory.adjustMatchType(OpenDirectory.kODMatchEqualTo, True),
+                OpenDirectory.kODMatchInsensitiveEqualTo
+            )
+            self.assertEquals(
+                opendirectory.adjustMatchType(OpenDirectory.kODMatchContains, False),
+                OpenDirectory.kODMatchContains
+            )
+            self.assertEquals(
+                opendirectory.adjustMatchType(OpenDirectory.kODMatchContains, True),
+                OpenDirectory.kODMatchInsensitiveContains
+            )
+
+        def test_getNodeAttributes(self):
+
+            directory = opendirectory.odInit("/Search")
+            results = opendirectory.getNodeAttributes(directory, "/Search", [OpenDirectory.kODAttributeTypeSearchPath])
+            self.assertTrue("/Local/Default" in results[OpenDirectory.kODAttributeTypeSearchPath])
+            self.assertTrue("/LDAPv3/127.0.0.1" in results[OpenDirectory.kODAttributeTypeSearchPath])
+
+        def test_listAllRecordsWithAttributes_list_master(self):
+
+            directory = opendirectory.odInit("/LDAPv3/127.0.0.1")
+            results = list(opendirectory.listAllRecordsWithAttributes_list(
+                directory,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            for recordName, info in setup_directory.masterUsers:
+                self.assertTrue(recordName in recordNames)
+
+        def test_listAllRecordsWithAttributes_list_local(self):
+
+            directory = opendirectory.odInit("/Local/Default")
+            results = list(opendirectory.listAllRecordsWithAttributes_list(
+                directory,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            for recordName, info in setup_directory.localUsers:
+                self.assertTrue(recordName in recordNames)
+
+
+        def test_queryRecordsWithAttribute_list_firstname_exact_insensitive_match(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrFirstName,
+                "betty",
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestbetty" in recordNames)
+
+        def test_queryRecordsWithAttribute_list_firstname_exact_insensitive_match_multitype(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrFirstName,
+                "betty",
+                dsattributes.eDSExact,
+                False,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestbetty" in recordNames)
+
+        def test_queryRecordsWithAttribute_list_firstname_begins_insensitive_match(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrFirstName,
+                "Amand",
+                dsattributes.eDSStartsWith,
+                True,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestamanda" in recordNames)
+
+        def test_queryRecordsWithAttribute_list_firstname_begins_insensitive_match_multitype(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrFirstName,
+                "Amand",
+                dsattributes.eDSStartsWith,
+                True,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestamanda" in recordNames)
+
+        def test_queryRecordsWithAttribute_list_firstname_contains_insensitive_match(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrFirstName,
+                "mand",
+                dsattributes.eDSContains,
+                True,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestamanda" in recordNames)
+
+        def test_queryRecordsWithAttribute_list_firstname_contains_insensitive_match_multitype(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrFirstName,
+                "mand",
+                dsattributes.eDSContains,
+                True,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestamanda" in recordNames)
+
+        def test_queryRecordsWithAttribute_list_lastname_exact_insensitive_match(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrLastName,
+                "test",
+                dsattributes.eDSExact,
+                True,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            for recordName, info in setup_directory.masterUsers:
+                self.assertTrue(recordName in recordNames)
+            for recordName, info in setup_directory.localUsers:
+                self.assertTrue(recordName in recordNames)
+
+        def test_queryRecordsWithAttribute_list_lastname_exact_insensitive_match_multitype(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrLastName,
+                "test",
+                dsattributes.eDSExact,
+                True,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            for recordName, info in setup_directory.masterUsers:
+                self.assertTrue(recordName in recordNames)
+            for recordName, info in setup_directory.localUsers:
+                self.assertTrue(recordName in recordNames)
+
+        def test_queryRecordsWithAttribute_list_lastname_begins_insensitive_match(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrLastName,
+                "tes",
+                dsattributes.eDSStartsWith,
+                True,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            for recordName, info in setup_directory.masterUsers:
+                self.assertTrue(recordName in recordNames)
+            for recordName, info in setup_directory.localUsers:
+                self.assertTrue(recordName in recordNames)
+
+        def test_queryRecordsWithAttribute_list_lastname_begins_insensitive_match_multitype(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrLastName,
+                "tes",
+                dsattributes.eDSStartsWith,
+                True,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            for recordName, info in setup_directory.masterUsers:
+                self.assertTrue(recordName in recordNames)
+            for recordName, info in setup_directory.localUsers:
+                self.assertTrue(recordName in recordNames)
+
+        def test_queryRecordsWithAttribute_list_lastname_contains_insensitive_match(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrLastName,
+                "es",
+                dsattributes.eDSContains,
+                True,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            for recordName, info in setup_directory.masterUsers:
+                self.assertTrue(recordName in recordNames)
+            for recordName, info in setup_directory.localUsers:
+                self.assertTrue(recordName in recordNames)
+
+        def test_queryRecordsWithAttribute_list_lastname_contains_insensitive_match_multitype(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrLastName,
+                "es",
+                dsattributes.eDSContains,
+                True,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            for recordName, info in setup_directory.masterUsers:
+                self.assertTrue(recordName in recordNames)
+            for recordName, info in setup_directory.localUsers:
+                self.assertTrue(recordName in recordNames)
+
+        def test_queryRecordsWithAttribute_list_email_begins_insensitive_match(self):
+            # This test won't pass until this is fixed: <rdar://problem/8608148>
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDSNAttrEMailAddress,
+                "aman",
+                dsattributes.eDSStartsWith,
+                True,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestamanda" in recordNames)
+
+        test_queryRecordsWithAttribute_list_email_begins_insensitive_match.todo = "This test won't pass until this is fixed: <rdar://problem/8608148>"
+
+        def test_queryRecordsWithAttribute_list_email_begins_insensitive_match_multitype(self):
+            # This test won't pass until this is fixed: <rdar://problem/8608148>
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDSNAttrEMailAddress,
+                "aman",
+                dsattributes.eDSStartsWith,
+                True,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestamanda" in recordNames)
+
+        test_queryRecordsWithAttribute_list_email_begins_insensitive_match_multitype.todo = "This test won't pass until this is fixed: <rdar://problem/8608148>"
+
+
+        def test_queryRecordsWithAttribute_list_guid_exact_sensitive_match_master(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                "9dc04a70-e6dd-11df-9492-0800200c9a66",
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            # OD Master user:
+            self.assertTrue("odtestamanda" in recordNames)
+
+        def test_queryRecordsWithAttribute_list_guid_exact_sensitive_match_multitype_master(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                "9dc04a70-e6dd-11df-9492-0800200c9a66",
+                dsattributes.eDSExact,
+                False,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            # OD Master user:
+            self.assertTrue("odtestamanda" in recordNames)
+
+
+        def test_queryRecordsWithAttribute_list_guid_exact_sensitive_match_local(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                "9dc04a74-e6dd-11df-9492-0800200c9a66",
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeUsers,
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            # Local user:
+            self.assertTrue("odtestalbert" in recordNames)
+
+
+        def test_queryRecordsWithAttribute_list_guid_exact_sensitive_match_multitype_local(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                "9dc04a74-e6dd-11df-9492-0800200c9a66",
+                dsattributes.eDSExact,
+                False,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            # Local user:
+            self.assertTrue("odtestalbert" in recordNames)
+
+
+
+        def test_queryRecordsWithAttribute_list_groupMembers_recordName_master(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDSNAttrRecordName,
+                "odtestgrouptop",
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [
+                    dsattributes.kDSNAttrGroupMembers,
+                    dsattributes.kDSNAttrNestedGroups,
+                ],
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestgrouptop" in recordNames)
+            groupMembers = results[0][1][dsattributes.kDSNAttrGroupMembers]
+            self.assertEquals(
+                groupMembers,
+                setup_directory.masterGroups[0][1][OpenDirectory.kODAttributeTypeGroupMembers]
+            )
+
+        def test_queryRecordsWithAttribute_list_groupMembers_recordName_local(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDSNAttrRecordName,
+                "odtestsubgroupa",
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [
+                    dsattributes.kDSNAttrGroupMembers,
+                    dsattributes.kDSNAttrNestedGroups,
+                ],
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestsubgroupa" in recordNames)
+            groupMembers = results[0][1][dsattributes.kDSNAttrGroupMembers]
+            self.assertEquals(
+                groupMembers,
+                setup_directory.localGroups[0][1][OpenDirectory.kODAttributeTypeGroupMembers]
+            )
+
+
+        def test_queryRecordsWithAttribute_list_groupMembers_guid_master(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                "6c6cd280-e6e3-11df-9492-0800200c9a66",
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [
+                    dsattributes.kDSNAttrGroupMembers,
+                    dsattributes.kDSNAttrNestedGroups,
+                ],
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestgrouptop" in recordNames)
+            groupMembers = results[0][1][dsattributes.kDSNAttrGroupMembers]
+            self.assertEquals(
+                groupMembers,
+                setup_directory.masterGroups[0][1][OpenDirectory.kODAttributeTypeGroupMembers]
+            )
+
+        def test_queryRecordsWithAttribute_list_groupMembers_guid_local(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                "6c6cd281-e6e3-11df-9492-0800200c9a66",
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [
+                    dsattributes.kDSNAttrGroupMembers,
+                    dsattributes.kDSNAttrNestedGroups,
+                ],
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestsubgroupa" in recordNames)
+            groupMembers = results[0][1][dsattributes.kDSNAttrGroupMembers]
+            self.assertEquals(
+                groupMembers,
+                setup_directory.localGroups[0][1][OpenDirectory.kODAttributeTypeGroupMembers]
+            )
+
+
+        def test_queryRecordsWithAttribute_list_groupsForGUID(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDSNAttrGroupMembers,
+                "9dc04a70-e6dd-11df-9492-0800200c9a66",
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [
+                    dsattributes.kDS1AttrGeneratedUID,
+                ],
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertTrue("odtestgrouptop" in recordNames)
+
+
+            results = list(opendirectory.queryRecordsWithAttribute_list(
+                directory,
+                dsattributes.kDSNAttrNestedGroups,
+                "9dc04a70-e6dd-11df-9492-0800200c9a66",
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [
+                    dsattributes.kDS1AttrGeneratedUID,
+                ],
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            self.assertEquals([], recordNames)
+
+        def test_queryRecordsWithAttributes_list_master(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            expressions = [
+                dsquery.match(dsattributes.kDS1AttrDistinguishedName, "aman", "starts-with"),
+                dsquery.match(dsattributes.kDS1AttrFirstName, "amanda", "equals"),
+                dsquery.match(dsattributes.kDS1AttrLastName, "es", "contains"),
+                dsquery.match(dsattributes.kDSNAttrEMailAddress, "amanda@", "starts-with"),
+            ]
+
+            compound = dsquery.expression(dsquery.expression.OR, expressions).generate()
+
+            results = list(opendirectory.queryRecordsWithAttributes_list(
+                directory,
+                compound,
+                True,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            # Master user:
+            self.assertTrue("odtestamanda" in recordNames)
+
+        def test_queryRecordsWithAttributes_list_local(self):
+
+            directory = opendirectory.odInit("/Search")
+
+            expressions = [
+                dsquery.match(dsattributes.kDS1AttrDistinguishedName, "bil", "starts-with"),
+                dsquery.match(dsattributes.kDS1AttrFirstName, "Bill", "equals"),
+                dsquery.match(dsattributes.kDS1AttrLastName, "es", "contains"),
+                dsquery.match(dsattributes.kDSNAttrEMailAddress, "bill@", "starts-with"),
+            ]
+
+            compound = dsquery.expression(dsquery.expression.OR, expressions).generate()
+
+            results = list(opendirectory.queryRecordsWithAttributes_list(
+                directory,
+                compound,
+                True,
+                [
+                    dsattributes.kDSStdRecordTypeUsers,
+                    dsattributes.kDSStdRecordTypeGroups,
+                    dsattributes.kDSStdRecordTypeResources,
+                    dsattributes.kDSStdRecordTypePlaces,
+                ],
+                USER_ATTRIBUTES,
+                count=0
+            ))
+            recordNames = [x[0] for x in results]
+            # Local user:
+            self.assertTrue("odtestbill" in recordNames)
+
+
+        def test_getUserRecord_existing(self):
+            directory = opendirectory.odInit("/Search")
+            record = opendirectory.getUserRecord(directory, "odtestbill")
+            self.assertNotEquals(record, None)
+
+        def test_getUserRecord_missing(self):
+            directory = opendirectory.odInit("/Search")
+            record = opendirectory.getUserRecord(directory, "i_do_not_exist")
+            self.assertEquals(record, None)
+
+        def test_basicAuth_master(self):
+
+            directory = opendirectory.odInit("/Search")
+            result = opendirectory.authenticateUserBasic(directory,
+                "/LDAPv3/127.0.0.1", "odtestamanda", "password")
+            self.assertTrue(result)
+
+        test_basicAuth_master.todo = "Not assigning passwords correctly, apparently"
+
+        def test_basicAuth_local(self):
+
+            directory = opendirectory.odInit("/Search")
+            result = opendirectory.authenticateUserBasic(directory,
+                "/Local/Default", "odtestalbert", "password")
+            self.assertTrue(result)
+
+        test_basicAuth_local.todo = "Not assigning passwords correctly, apparently"

Modified: CalendarServer/trunk/calendarserver/tools/resources.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/resources.py	2010-11-03 15:50:43 UTC (rev 6560)
+++ CalendarServer/trunk/calendarserver/tools/resources.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -30,10 +30,15 @@
 from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
 from twistedcaldav.directory.directory import DirectoryService, DirectoryError
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
-import opendirectory, dsattributes
 import os
 import sys
 
+# TODO: Temporary means of switching to PyObjC version
+if os.path.exists("/tmp/calendarserver_use_pyobjc"):
+    from calendarserver.od import opendirectory, dsattributes
+else:
+    import opendirectory, dsattributes
+
 __all__ = [ "migrateResources", ]
 
 def usage():
@@ -168,17 +173,16 @@
     attrs = [
         dsattributes.kDS1AttrGeneratedUID,
         dsattributes.kDS1AttrDistinguishedName,
-        # NEED THIS? dsattributes.kDSNAttrServicesLocator,
     ]
 
     if verbose:
         print "Querying for all %s records" % (recordType,)
 
-    results = opendirectory.listAllRecordsWithAttributes_list(
+    results = list(opendirectory.listAllRecordsWithAttributes_list(
         sourceService.directory,
         recordType,
         attrs,
-    )
+    ))
 
     if verbose:
         print "Found %d records" % (len(results),)

Modified: CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2010-11-03 15:50:43 UTC (rev 6560)
+++ CalendarServer/trunk/twistedcaldav/directory/appleopendirectory.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -27,11 +27,7 @@
 import time
 from uuid import UUID
 
-import opendirectory
-import dsattributes
-import dsquery
-
-from twisted.internet.threads import deferToThread
+from twisted.internet.defer import succeed
 from twisted.cred.credentials import UsernamePassword
 from twext.web2.auth.digest import DigestedCredentials
 
@@ -43,6 +39,14 @@
 from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
 from twistedcaldav.directory.principal import cuAddressConverter
 
+# TODO: Temporary means of switching to PyObjC version
+import os
+if os.path.exists("/tmp/calendarserver_use_pyobjc"):
+    from calendarserver.od import opendirectory, dsattributes, dsquery
+else:
+    import opendirectory, dsattributes, dsquery
+
+
 class OpenDirectoryService(CachingDirectoryService):
     """
     OpenDirectory implementation of L{IDirectoryService}.
@@ -67,7 +71,6 @@
                         if C{False} they are not.
                         This should only be set to C{False} when doing unit tests.
         """
-
         defaults = {
             'node' : '/Search',
             'restrictEnabledRecords' : False,
@@ -238,22 +241,21 @@
 
         guids = set()
 
-        query = dsquery.match(dsattributes.kDSNAttrGroupMembers, guid, dsattributes.eDSExact)
         try:
             self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
                 self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
+                dsattributes.kDSNAttrGroupMembers,
+                guid,
+                dsattributes.eDSExact,
                 False,
                 recordType,
                 attrs,
             ))
             results = opendirectory.queryRecordsWithAttribute_list(
                 self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
+                dsattributes.kDSNAttrGroupMembers,
+                guid,
+                dsattributes.eDSExact,
                 False,
                 recordType,
                 attrs,
@@ -269,22 +271,21 @@
             if recordGUID:
                 guids.add(recordGUID)
 
-        query = dsquery.match(dsattributes.kDSNAttrNestedGroups, guid, dsattributes.eDSExact)
         try:
             self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
                 self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
+                dsattributes.kDSNAttrNestedGroups,
+                guid,
+                dsattributes.eDSExact,
                 False,
                 recordType,
                 attrs,
             ))
             results = opendirectory.queryRecordsWithAttribute_list(
                 self.directory,
-                query.attribute,
-                query.value,
-                query.matchType,
+                dsattributes.kDSNAttrNestedGroups,
+                guid,
+                dsattributes.eDSExact,
                 False,
                 recordType,
                 attrs,
@@ -482,31 +483,37 @@
                 self.log_debug("Calling OD: Types %s, Field %s, Value %s, Match %s, Caseless %s" %
                     (recordTypes, ODField, value, matchType, caseless))
 
-                queryResults = lookupMethod(
-                    directory,
-                    ODField,
-                    value,
-                    comparison,
-                    caseless,
-                    recordTypes,
-                    attrs,
-                )
+                try:
+                    queryResults = lookupMethod(
+                        directory,
+                        ODField,
+                        value,
+                        comparison,
+                        caseless,
+                        recordTypes,
+                        attrs,
+                    )
 
-                if operand == dsquery.expression.OR:
-                    for recordName, data in queryResults:
-                        guid = data.get(dsattributes.kDS1AttrGeneratedUID, None)
-                        if guid:
-                            byGUID[guid] = (recordName, data)
-                else: # AND
-                    newSet = set()
-                    for recordName, data in queryResults:
-                        guid = data.get(dsattributes.kDS1AttrGeneratedUID, None)
-                        if guid:
-                            byGUID[guid] = (recordName, data)
-                            newSet.add(guid)
+                    if operand == dsquery.expression.OR:
+                        for recordName, data in queryResults:
+                            guid = data.get(dsattributes.kDS1AttrGeneratedUID, None)
+                            if guid:
+                                byGUID[guid] = (recordName, data)
+                    else: # AND
+                        newSet = set()
+                        for recordName, data in queryResults:
+                            guid = data.get(dsattributes.kDS1AttrGeneratedUID, None)
+                            if guid:
+                                byGUID[guid] = (recordName, data)
+                                newSet.add(guid)
 
-                    sets.append(newSet)
+                        sets.append(newSet)
 
+                except opendirectory.ODError, e:
+                    self.log_error("Ignoring OD Error: %d %s" %
+                        (e.message[1], e.message[0]))
+                    continue
+
             if operand == dsquery.expression.OR:
                 return byGUID.values()
 
@@ -531,8 +538,7 @@
 
         queries = buildQueries(recordTypes, fields, self._ODFields)
 
-        deferred = deferToThread(
-            multiQuery,
+        results = multiQuery(
             self.directory,
             queries,
             [
@@ -548,11 +554,9 @@
             ],
             operand
         )
-        deferred.addCallback(collectResults)
-        return deferred
+        return succeed(collectResults(results))
 
 
-
     def queryDirectory(self, recordTypes, indexType, indexKey,
         lookupMethod=opendirectory.queryRecordsWithAttribute_list):
 
@@ -580,8 +584,6 @@
         else:
             caseInsensitive = False
 
-        query = dsquery.match(queryattr, indexKey, dsattributes.eDSExact)
-
         results = []
         for recordType in recordTypes:
 
@@ -618,9 +620,9 @@
                 try:
                     self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
                         self.directory,
-                        query.attribute,
-                        query.value,
-                        query.matchType,
+                        queryattr,
+                        indexKey,
+                        dsattributes.eDSExact,
                         caseInsensitive,
                         listRecordTypes,
                         attrs,
@@ -628,9 +630,9 @@
                     results.extend(
                         lookupMethod(
                             self.directory,
-                            query.attribute,
-                            query.value,
-                            query.matchType,
+                            queryattr,
+                            indexKey,
+                            dsattributes.eDSExact,
                             caseInsensitive,
                             listRecordTypes,
                             attrs,

Modified: CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	2010-11-03 15:50:43 UTC (rev 6560)
+++ CalendarServer/trunk/twistedcaldav/directory/opendirectorybacker.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -35,15 +35,10 @@
 from tempfile import mkstemp, gettempdir
 from random import random
 
-import opendirectory
-import dsattributes
-
-from dsquery import match, expression
 from socket import getfqdn
 
 from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator
-from twisted.internet.threads import deferToThread
+from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator, succeed
 from twext.python.filepath import CachingFilePath as FilePath
 from twext.web2.dav import davxml
 from twext.web2.dav.element.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
@@ -64,6 +59,11 @@
 from xmlrpclib import datetime
 from vobject.vcard import Name, Address
 
+# TODO: Temporary means of switching to PyObjC version
+if os.path.exists("/tmp/calendarserver_use_pyobjc"):
+    from calendarserver.od import opendirectory, dsattributes, dsquery
+else:
+    import opendirectory, dsattributes, dsquery
 
 class OpenDirectoryBackingService(DirectoryService):
     """
@@ -439,11 +439,11 @@
                         recordTypes,
                         self.returnedAttributes,
                     ))
-                results = opendirectory.listAllRecordsWithAttributes_list(
+                results = list(opendirectory.listAllRecordsWithAttributes_list(
                         localNodeDirectory,
                         recordTypes,
                         self.returnedAttributes,
-                    )
+                    ))
             except opendirectory.ODError, ex:
                 self.log_error("Open Directory (node=%s) error: %s" % ("/Local/Default", str(ex)))
                 raise
@@ -521,7 +521,6 @@
         returnValue((records, limited, ))
 
 
-    @inlineCallbacks
     def _queryDirectory(self, query=None, attributes=None, maxRecords=0 ):
         
         startTime = time.time()
@@ -546,7 +545,7 @@
         for directory, node, recordType in directoryAndRecordTypes:
             try:
                 if query:
-                    if isinstance(query, match) and query.value is not "":
+                    if isinstance(query, dsquery.match) and query.value is not "":
                         self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r,%r)" % (
                             node,
                             query.attribute,
@@ -557,17 +556,17 @@
                             attributes,
                             maxRecords,
                         ))
-                        results = (yield deferToThread(
-                            opendirectory.queryRecordsWithAttribute_list,
-                            directory,
-                            query.attribute,
-                            query.value,
-                            query.matchType,
-                            False,
-                            recordType,
-                            attributes,
-                            maxRecords,
-                        ))
+                        results = list(
+                            opendirectory.queryRecordsWithAttribute_list(
+                                directory,
+                                query.attribute,
+                                query.value,
+                                query.matchType,
+                                False,
+                                recordType,
+                                attributes,
+                                maxRecords,
+                            ))
                     else:
                         self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r)" % (
                             node,
@@ -577,15 +576,15 @@
                             attributes,
                             maxRecords,
                         ))
-                        results = (yield deferToThread(
-                            opendirectory.queryRecordsWithAttributes_list,
-                            directory,
-                            query.generate(),
-                            False,
-                            recordType,
-                            attributes,
-                            maxRecords,
-                        ))
+                        results = list(
+                            opendirectory.queryRecordsWithAttributes_list(
+                                directory,
+                                query.generate(),
+                                False,
+                                recordType,
+                                attributes,
+                                maxRecords,
+                            ))
                 else:
                     self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r,%r)" % (
                         node,
@@ -593,13 +592,13 @@
                         attributes,
                         maxRecords,
                     ))
-                    results = (yield deferToThread(
-                        opendirectory.listAllRecordsWithAttributes_list,
-                        directory,
-                        recordType,
-                        attributes,
-                        maxRecords,
-                    ))
+                    results = list(
+                        opendirectory.listAllRecordsWithAttributes_list(
+                            directory,
+                            recordType,
+                            attributes,
+                            maxRecords,
+                        ))
             except opendirectory.ODError, ex:
                 self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
                 raise
@@ -614,7 +613,7 @@
 
         elaspedTime = time.time()-startTime
         self.log_info("Timing: Directory query: %.1f ms (%d records, %.2f records/sec)" % (elaspedTime*1000, len(allResults), len(allResults)/elaspedTime))
-        returnValue( allResults )
+        return succeed(allResults)
     
     def _getDSFilter(self, addressBookFilter):
         """
@@ -638,15 +637,15 @@
                     if constant or filterName in ("N" , "FN", "UID", ):
                         return (defined, [], [])     # all records have this property so no records do not have it
                     else:
-                        matchList = list(set([match(attrName, "", dsattributes.eDSStartsWith) for attrName in allAttrStrings]))
+                        matchList = list(set([dsquery.match(attrName, "", dsattributes.eDSStartsWith) for attrName in allAttrStrings]))
                         if defined:
                             return andOrExpression(allOf, queryAttributes, matchList)
                         else:
                             if len(matchList) > 1:
-                                expr = expression( expression.OR, matchList )
+                                expr = dsquery.expression( dsquery.expression.OR, matchList )
                             else:
                                 expr = matchList
-                            return (False, queryAttributes, [expression( expression.NOT, expr),])
+                            return (False, queryAttributes, [dsquery.expression( dsquery.expression.NOT, expr),])
                     #end isNotDefinedExpression()
     
     
@@ -654,7 +653,7 @@
                     #print("andOrExpression(propFilterAllOf=%r, queryAttributes%r, matchList%r)" % (propFilterAllOf, queryAttributes, matchList))
                     if propFilterAllOf and len(matchList):
                         # add OR expression because parent will AND
-                        return (False, queryAttributes, [expression( expression.OR, matchList),])
+                        return (False, queryAttributes, [dsquery.expression( dsquery.expression.OR, matchList),])
                     else:
                         return (False, queryAttributes, matchList)
                     #end andOrExpression()
@@ -786,11 +785,11 @@
                                     else:
                                         if textMatchElement.negate:
                                             return (False, queryAttributes, 
-                                                    [expression(expression.NOT, match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact)),]
+                                                    [dsquery.expression(dsquery.expression.NOT, dsquery.match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact)),]
                                                     )
                                         else:
                                             return (False, queryAttributes, 
-                                                    [match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact),]
+                                                    [dsquery.match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact),]
                                                     )
                             
                             # use match_type where possible depending on property/attribute mapping
@@ -807,16 +806,16 @@
                             
                             matchList = []
                             for matchString in matchStrings:
-                                matchList += [match(attrName, matchString, matchType) for attrName in stringAttrStrs]
+                                matchList += [dsquery.match(attrName, matchString, matchType) for attrName in stringAttrStrs]
                             
                             matchList = list(set(matchList))
     
                             if textMatchElement.negate:
                                 if len(matchList) > 1:
-                                    expr = expression( expression.OR, matchList )
+                                    expr = dsquery.expression( dsquery.expression.OR, matchList )
                                 else:
                                     expr = matchList
-                                return (False, queryAttributes, [expression( expression.NOT, expr),])
+                                return (False, queryAttributes, [dsquery.expression( dsquery.expression.NOT, expr),])
                             else:
                                 return andOrExpression(propFilterAllOf, queryAttributes, matchList)
     
@@ -873,7 +872,7 @@
     
 
                 if (len(propFilterExpressionList) > 1) and (filterAllOf != propFilterAllOf):
-                    propFilterExpressions = [expression(expression.AND if propFilterAllOf else expression.OR , list(set(propFilterExpressionList)))] # remove duplicates
+                    propFilterExpressions = [dsquery.expression(dsquery.expression.AND if propFilterAllOf else dsquery.expression.OR , list(set(propFilterExpressionList)))] # remove duplicates
                 else:
                     propFilterExpressions = list(set(propFilterExpressionList))
                 
@@ -902,7 +901,7 @@
                 expressions += propExpression
 
             if len(expressions) > 1:
-                expr = expression(expression.AND if filterAllOf else expression.OR , list(set(expressions))) # remove duplicates
+                expr = dsquery.expression(dsquery.expression.AND if filterAllOf else dsquery.expression.OR , list(set(expressions))) # remove duplicates
             elif len(expressions):
                 expr = expressions[0]
             else:

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_buildquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_buildquery.py	2010-11-03 15:50:43 UTC (rev 6560)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_buildquery.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -16,7 +16,7 @@
 
 from twistedcaldav.test.util import TestCase
 from twistedcaldav.directory.appleopendirectory import buildQueries, OpenDirectoryService
-import dsattributes
+from calendarserver.od import dsattributes
 
 class BuildQueryTests(TestCase):
 

Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2010-11-03 15:50:43 UTC (rev 6560)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_opendirectory.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -25,7 +25,7 @@
     from twisted.internet.defer import inlineCallbacks
     from twistedcaldav.directory.directory import DirectoryService
     from twistedcaldav.directory.appleopendirectory import OpenDirectoryRecord
-    import dsattributes
+    from calendarserver.od import dsattributes
 
     # Wonky hack to prevent unclean reactor shutdowns
     class DummyReactor(object):

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-11-03 15:50:43 UTC (rev 6560)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-11-03 19:51:58 UTC (rev 6561)
@@ -318,7 +318,7 @@
         "ProcessCount": 0,
         "MinProcessCount": 2,
         "PerCPU": 1,
-        "PerGB": 2,
+        "PerGB": 1,
         "StaggeredStartup": {
             "Enabled": False,
             "Interval": 15,
@@ -598,7 +598,7 @@
     },
 
     "EnableKeepAlive": True,
-    
+
     "Includes": [],     # Other plists to parse after this one
 }
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20101103/e982965c/attachment-0001.html>


More information about the calendarserver-changes mailing list