[CalendarServer-changes] [5434] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Thu Apr 1 11:16:22 PDT 2010


Revision: 5434
          http://trac.macosforge.org/projects/calendarserver/changeset/5434
Author:   sagen at apple.com
Date:     2010-04-01 11:16:19 -0700 (Thu, 01 Apr 2010)
Log Message:
-----------
Landing deprovision branch -- adds calendarserver_purge_principals

Modified Paths:
--------------
    CalendarServer/trunk/bin/calendarserver_purge_events
    CalendarServer/trunk/calendarserver/tools/purge.py
    CalendarServer/trunk/calendarserver/tools/test/test_purge.py
    CalendarServer/trunk/twistedcaldav/directory/aggregate.py
    CalendarServer/trunk/twistedcaldav/test/util.py

Added Paths:
-----------
    CalendarServer/trunk/bin/calendarserver_purge_principals
    CalendarServer/trunk/doc/calendarserver_purge_principals.8

Modified: CalendarServer/trunk/bin/calendarserver_purge_events
===================================================================
--- CalendarServer/trunk/bin/calendarserver_purge_events	2010-04-01 17:48:04 UTC (rev 5433)
+++ CalendarServer/trunk/bin/calendarserver_purge_events	2010-04-01 18:16:19 UTC (rev 5434)
@@ -40,5 +40,5 @@
 
         sys.argv[1:1] = ["-f", join(home, "conf", "caldavd-dev.plist")]
 
-    from calendarserver.tools.purge import main_purge
-    main_purge()
+    from calendarserver.tools.purge import main_purge_events
+    main_purge_events()

Added: CalendarServer/trunk/bin/calendarserver_purge_principals
===================================================================
--- CalendarServer/trunk/bin/calendarserver_purge_principals	                        (rev 0)
+++ CalendarServer/trunk/bin/calendarserver_purge_principals	2010-04-01 18:16:19 UTC (rev 5434)
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+##
+# 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.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+    if "PYTHONPATH" in globals():
+        sys.path.insert(0, PYTHONPATH)
+    else:
+        from os.path import dirname, abspath, join
+        from subprocess import Popen, PIPE
+
+        home = dirname(dirname(abspath(__file__)))
+        run = join(home, "run")
+
+        child = Popen((run, "-p"), stdout=PIPE)
+        path, stderr = child.communicate()
+
+        path = path.rstrip("\n")
+
+        if child.wait() == 0:
+            sys.path[0:0] = path.split(":")
+
+        sys.argv[1:1] = ["-f", join(home, "conf", "caldavd-dev.plist")]
+
+    from calendarserver.tools.purge import main_purge_principals
+    main_purge_principals()

Modified: CalendarServer/trunk/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/purge.py	2010-04-01 17:48:04 UTC (rev 5433)
+++ CalendarServer/trunk/calendarserver/tools/purge.py	2010-04-01 18:16:19 UTC (rev 5434)
@@ -16,8 +16,10 @@
 # limitations under the License.
 ##
 
+from cStringIO import StringIO
 from calendarserver.tap.util import FakeRequest
 from calendarserver.tap.util import getRootResource
+from calendarserver.tools.principals import removeProxy
 from calendarserver.tools.util import loadConfig, setupMemcached, setupNotifications
 from datetime import date, timedelta, datetime
 from getopt import getopt, GetoptError
@@ -31,14 +33,14 @@
 from twistedcaldav import caldavxml
 from twistedcaldav.caldavxml import TimeRange
 from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.directory.directory import DirectoryError
+from twistedcaldav.directory.directory import DirectoryError, DirectoryRecord
 from twistedcaldav.method.delete_common import DeleteResource
 import os
 import sys
 
 log = Logger()
 
-def usage_purge(e=None):
+def usage_purge_events(e=None):
 
     name = os.path.basename(sys.argv[0])
     print "usage: %s [options]" % (name,)
@@ -58,10 +60,64 @@
     else:
         sys.exit(0)
 
+def usage_purge_principal(e=None):
 
-def main_purge():
+    name = os.path.basename(sys.argv[0])
+    print "usage: %s [options]" % (name,)
+    print ""
+    print "  Remove a principal's events from the calendar server"
+    print ""
+    print "options:"
+    print "  -f --config <path>: Specify caldavd.plist configuration path"
+    print "  -h --help: print this help and exit"
+    print "  -n --dry-run: only calculate how many events to purge"
+    print "  -v --verbose: print progress information"
+    print ""
 
+    if e:
+        sys.exit(64)
+    else:
+        sys.exit(0)
+
+
+
+def shared_main(configFileName, method, *args, **kwds):
+
     try:
+        loadConfig(configFileName)
+
+        # Shed privileges
+        if config.UserName and config.GroupName and os.getuid() == 0:
+            uid = getpwnam(config.UserName).pw_uid
+            gid = getgrnam(config.GroupName).gr_gid
+            switchUID(uid, uid, gid)
+
+        os.umask(config.umask)
+
+        try:
+            rootResource = getRootResource(config)
+            directory = rootResource.getDirectory()
+        except DirectoryError, e:
+            print "Error: %s" % (e,)
+            return
+        setupMemcached(config)
+        setupNotifications(config)
+    except ConfigurationError, e:
+        print "Error: %s" % (e,)
+        return
+
+
+    #
+    # Start the reactor
+    #
+    reactor.callLater(0.1, callThenStop, method, directory,
+        rootResource, *args, **kwds)
+
+    reactor.run()
+
+def main_purge_events():
+
+    try:
         (optargs, args) = getopt(
             sys.argv[1:], "d:f:hnv", [
                 "days=",
@@ -72,7 +128,7 @@
             ],
         )
     except GetoptError, e:
-        usage_purge(e)
+        usage_purge_events(e)
 
     #
     # Get configuration
@@ -84,14 +140,14 @@
 
     for opt, arg in optargs:
         if opt in ("-h", "--help"):
-            usage_purge()
+            usage_purge_events()
 
         elif opt in ("-d", "--days"):
             try:
                 days = int(arg)
             except ValueError, e:
                 print "Invalid value for --days: %s" % (arg,)
-                usage_purge(e)
+                usage_purge_events(e)
 
         elif opt in ("-v", "--verbose"):
             verbose = True
@@ -107,38 +163,52 @@
 
     cutoff = (date.today()-timedelta(days=days)).strftime("%Y%m%dT000000Z")
 
-    try:
-        loadConfig(configFileName)
+    shared_main(configFileName, purgeOldEvents, cutoff, verbose=verbose,
+        dryrun=dryrun)
 
-        # Shed privileges
-        if config.UserName and config.GroupName and os.getuid() == 0:
-            uid = getpwnam(config.UserName).pw_uid
-            gid = getgrnam(config.GroupName).gr_gid
-            switchUID(uid, uid, gid)
 
-        os.umask(config.umask)
+def main_purge_principals():
 
-        try:
-            rootResource = getRootResource(config)
-            directory = rootResource.getDirectory()
-        except DirectoryError, e:
-            print "Error: %s" % (e,)
-            return
-        setupMemcached(config)
-        setupNotifications(config)
-    except ConfigurationError, e:
-        print "Error: %s" % (e,)
-        return
+    try:
+        (optargs, args) = getopt(
+            sys.argv[1:], "f:hnv", [
+                "dry-run",
+                "config=",
+                "help",
+                "verbose",
+            ],
+        )
+    except GetoptError, e:
+        usage_purge_principal(e)
 
-
     #
-    # Start the reactor
+    # Get configuration
     #
-    reactor.callLater(0.1, callThenStop, purgeOldEvents, directory,
-        rootResource, cutoff, verbose=verbose, dryrun=dryrun)
+    configFileName = None
+    dryrun = False
+    verbose = False
 
-    reactor.run()
+    for opt, arg in optargs:
+        if opt in ("-h", "--help"):
+            usage_purge_principal()
 
+        elif opt in ("-v", "--verbose"):
+            verbose = True
+
+        elif opt in ("-n", "--dry-run"):
+            dryrun = True
+
+        elif opt in ("-f", "--config"):
+            configFileName = arg
+
+        else:
+            raise NotImplementedError(opt)
+
+    # args is a list of guids
+
+    shared_main(configFileName, purgeGUIDs, args, verbose=verbose, dryrun=dryrun)
+
+
 @inlineCallbacks
 def callThenStop(method, *args, **kwds):
     try:
@@ -258,21 +328,44 @@
 
 
 @inlineCallbacks
-def purgeGUID(guid, directory, root):
+def purgeGUIDs(directory, root, guids, verbose=False, dryrun=False):
+    total = 0
 
+    for guid in guids:
+        count, allAssignments[guid] = (yield purgeGUID(guid, directory, root,
+            verbose=verbose, dryrun=dryrun))
+        total += count
+
+
+    # TODO: figure out what to do with the purged proxy assignments...
+    # ...print to stdout?
+    # ...save in a file?
+
+    returnValue(total)
+
+
+ at inlineCallbacks
+def purgeGUID(guid, directory, root, verbose=False, dryrun=False):
+
     # Does the record exist?
     record = directory.recordWithGUID(guid)
     if record is None:
         # The user has already been removed from the directory service.  We
         # need to fashion a temporary, fake record
-        # FIXME: implement the fake record
-        pass
 
+        # FIXME: probaby want a more elegant way to accomplish this,
+        # since it requires the aggregate directory to examine these first:
+        record = DirectoryRecord(directory, "users", guid, shortNames=(guid,),
+            enabledForCalendaring=True)
+        record.enabled = True
+        directory._tmpRecords["shortNames"][guid] = record
+        directory._tmpRecords["guids"][guid] = record
+
     principalCollection = directory.principalCollection
     principal = principalCollection.principalForRecord(record)
     calendarHome = principal.calendarHome()
 
-    # Anything in the past should be deleted without implicit scheduling
+    # Anything in the past is left alone
     now = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
     filter =  caldavxml.Filter(
           caldavxml.ComponentFilter(
@@ -290,27 +383,43 @@
         collection = calendarHome.getChild(collName)
         if collection.isCalendarCollection():
 
-            # To compute past and ongoing events...
-
-            # ...first start with all events...
-            allEvents = set(collection.listChildren())
-
-            ongoingEvents = set()
-
-            # ...and find those that appear *after* the given cutoff
             for name, uid, type in collection.index().indexedSearch(filter):
-                ongoingEvents.add(name)
-
-            for name in allEvents:
                 resource = collection.getChild(name)
                 uri = "/calendars/__uids__/%s/%s/%s" % (
                     record.uid,
                     collName,
                     name
                 )
-
-                (yield deleteResource(root, collection, resource,
-                    uri, guid, implicit=(name in ongoingEvents)))
+                if not dryrun:
+                    (yield deleteResource(root, collection, resource,
+                        uri, guid, implicit=True))
                 count += 1
 
-    returnValue(count)
+    if not dryrun:
+        assignments = (yield purgeProxyAssignments(principal))
+
+    returnValue((count, assignments))
+
+
+ at inlineCallbacks
+def purgeProxyAssignments(principal):
+
+    assignments = []
+
+    for proxyType in ("read", "write"):
+
+        proxyFor = (yield principal.proxyFor(proxyType == "write"))
+        for other in proxyFor:
+            assignments.append((principal.record.guid, proxyType, other.record.guid))
+            (yield removeProxy(other, principal))
+
+        subPrincipal = principal.getChild("calendar-proxy-" + proxyType)
+        proxies = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+        for other in proxies.children:
+            assignments.append((str(other).split("/")[3], proxyType, principal.record.guid))
+
+        (yield subPrincipal.writeProperty(davxml.GroupMemberSet(), None))
+
+    returnValue(assignments)
+
+

Modified: CalendarServer/trunk/calendarserver/tools/test/test_purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_purge.py	2010-04-01 17:48:04 UTC (rev 5433)
+++ CalendarServer/trunk/calendarserver/tools/test/test_purge.py	2010-04-01 18:16:19 UTC (rev 5434)
@@ -15,13 +15,16 @@
 ##
 
 from calendarserver.tap.util import getRootResource
-from calendarserver.tools.purge import purgeOldEvents, purgeGUID
+from calendarserver.tools.principals import addProxy
+from calendarserver.tools.purge import purgeOldEvents, purgeGUID, purgeProxyAssignments
 from datetime import datetime, timedelta
 from twext.python.filepath import CachingFilePath as FilePath
 from twext.python.plistlib import readPlistFromString
+from twext.web2.dav import davxml
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, Deferred, returnValue
 from twistedcaldav.config import config
+from twistedcaldav.directory.directory import DirectoryRecord
 from twistedcaldav.test.util import TestCase, CapturingProcessProtocol
 import os
 import xml
@@ -411,10 +414,62 @@
 
         returnValue(plist)
 
+
     @inlineCallbacks
-    def test_purgeGUID(self):
-        # deprovision, add an event
+    def test_purgeProxies(self):
 
+        # Set up fake user
+        purging = "5D6ABA3C-3446-4340-8083-7E37C5BC0B26"
+        record = DirectoryRecord(self.directory, "users", purging,
+            shortNames=(purging,), enabledForCalendaring=True)
+        record.enabled = True # Enabling might not be required here
+        self.directory._tmpRecords["shortNames"][purging] = record
+        self.directory._tmpRecords["guids"][purging] = record
+        pc = self.directory.principalCollection
+        purgingPrincipal = pc.principalForRecord(record)
+
+        keeping = "291C2C29-B663-4342-8EA1-A055E6A04D65"
+        keepingPrincipal = pc.principalForUID(keeping)
+
+        def getProxies(principal, proxyType):
+            subPrincipal = principal.getChild("calendar-proxy-" + proxyType)
+            return subPrincipal.readProperty(davxml.GroupMemberSet, None)
+
+        # Add purgingPrincipal as a proxy for keepingPrincipal
+        (yield addProxy(keepingPrincipal, "write", purgingPrincipal))
+
+        # Add keepingPrincipal as a proxy for purgingPrincipal
+        (yield addProxy(purgingPrincipal, "write", keepingPrincipal))
+
+        # Verify the proxy assignments
+        membersProperty = (yield getProxies(keepingPrincipal, "write"))
+        self.assertEquals(len(membersProperty.children), 1)
+        self.assertEquals(membersProperty.children[0],
+            "/principals/__uids__/5D6ABA3C-3446-4340-8083-7E37C5BC0B26/")
+        membersProperty = (yield getProxies(keepingPrincipal, "read"))
+        self.assertEquals(len(membersProperty.children), 0)
+
+        membersProperty = (yield getProxies(purgingPrincipal, "write"))
+        self.assertEquals(len(membersProperty.children), 1)
+        self.assertEquals(membersProperty.children[0],
+            "/principals/__uids__/291C2C29-B663-4342-8EA1-A055E6A04D65/")
+        membersProperty = (yield getProxies(purgingPrincipal, "read"))
+        self.assertEquals(len(membersProperty.children), 0)
+
+        # Purging the guid should clear out proxy assignments
+
+        assignments = (yield purgeProxyAssignments(purgingPrincipal))
+        self.assertTrue(("5D6ABA3C-3446-4340-8083-7E37C5BC0B26", "write", "291C2C29-B663-4342-8EA1-A055E6A04D65") in assignments)
+        self.assertTrue(("291C2C29-B663-4342-8EA1-A055E6A04D65", "write", "5D6ABA3C-3446-4340-8083-7E37C5BC0B26") in assignments)
+
+        membersProperty = (yield getProxies(keepingPrincipal, "write"))
+        self.assertEquals(len(membersProperty.children), 0)
+        membersProperty = (yield getProxies(purgingPrincipal, "write"))
+        self.assertEquals(len(membersProperty.children), 0)
+
+    @inlineCallbacks
+    def test_purgeExistingGUID(self):
+
         # Deprovisioned user is E9E78C86-4829-4520-A35D-70DDADAB2092
         # Keeper user is        291C2C29-B663-4342-8EA1-A055E6A04D65
 
@@ -464,12 +519,11 @@
             },
         }
         self.createHierarchy(before, config.DocumentRoot)
-        count = (yield purgeGUID("E9E78C86-4829-4520-A35D-70DDADAB2092",
+        count, assignments = (yield purgeGUID(
+            "E9E78C86-4829-4520-A35D-70DDADAB2092",
             self.directory, self.rootResource))
 
-        # print config.DocumentRoot
-        # import pdb; pdb.set_trace()
-        self.assertEquals(count, 3)
+        self.assertEquals(count, 2)
 
         after = {
             "__uids__" : {
@@ -480,6 +534,9 @@
                                 ".db.sqlite": {
                                     "@contents" : None, # ignore contents
                                 },
+                                "noninvite.ics": {
+                                    "@contents" : NON_INVITE_ICS,
+                                },
                             },
                         },
                     },
@@ -529,9 +586,326 @@
         )
 
 
+    @inlineCallbacks
+    def test_purgeNonExistentGUID(self):
+
+        before = {
+            "calendars" : {
+                "__uids__" : {
+                    "1C" : {
+                        "B4" : {
+                            "1CB4378B-DD76-462D-B4D4-BD131FE89243" : {
+                                "calendar": {
+                                    "@xattrs" :
+                                    {
+                                        resourceAttr : collectionType,
+                                    },
+                                    # non-repeating, non-invite, in the past
+                                    # = untouched
+                                    "noninvite_past.ics": {
+                                        "@contents" : NON_INVITE_PAST_ICS,
+                                    },
+                                    # non-repeating, non-invite, in the future
+                                    # = removed
+                                    "noninvite_future.ics": {
+                                        "@contents" : NON_INVITE_FUTURE_ICS,
+                                    },
+                                    "organizer.ics": {
+                                        "@contents" : ORGANIZER_ICS_2,
+                                    },
+                                    "attendee.ics": {
+                                        "@contents" : ATTENDEE_ICS_2,
+                                    },
+                                    "repeating_organizer.ics": {
+                                        "@contents" : REPEATING_ORGANIZER_ICS,
+                                    },
+                                },
+                            },
+                        },
+                    },
+                    "29" : {
+                        "1C" : {
+                            "291C2C29-B663-4342-8EA1-A055E6A04D65" : {
+                                "calendar": {
+                                    "@xattrs" :
+                                    {
+                                        resourceAttr : collectionType,
+                                    },
+                                    "organizer.ics": {
+                                        "@contents" : ORGANIZER_ICS_2,
+                                    },
+                                    "attendee.ics": {
+                                        "@contents" : ATTENDEE_ICS_2,
+                                    },
+                                    "repeating_organizer.ics": {
+                                        "@contents" : REPEATING_ORGANIZER_ICS,
+                                    },
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        }
+        self.createHierarchy(before, config.DocumentRoot)
+        count, assignments = (yield purgeGUID(
+            "1CB4378B-DD76-462D-B4D4-BD131FE89243",
+            self.directory, self.rootResource))
+
+        self.assertEquals(count, 4)
+
+        after = {
+            "__uids__" : {
+                "1C" : {
+                    "B4" : {
+                        "1CB4378B-DD76-462D-B4D4-BD131FE89243" : {
+                            "calendar": {
+                                ".db.sqlite": {
+                                    "@contents" : None, # ignore contents
+                                },
+                                "noninvite_past.ics": {
+                                    "@contents" : NON_INVITE_PAST_ICS,
+                                },
+                            },
+                        },
+                    },
+                },
+                "29" : {
+                    "1C" : {
+                        "291C2C29-B663-4342-8EA1-A055E6A04D65" : {
+                            "inbox": {
+                                ".db.sqlite": {
+                                    "@contents" : None, # ignore contents
+                                },
+                                "*.ics/UID:7ED97931-9A19-4596-9D4D-52B36D6AB803": {
+                                    "@contents" : (
+                                        "METHOD:CANCEL",
+                                        ),
+                                },
+                                "*.ics/UID:1974603C-B2C0-4623-92A0-2436DEAB07EF": {
+                                    "@contents" : (
+                                        "METHOD:REPLY",
+                                        "ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED:urn:uuid:1CB4378B-DD76-462D-B\r\n 4D4-BD131FE89243",
+                                        ),
+                                },
+                                "*.ics/UID:8ED97931-9A19-4596-9D4D-52B36D6AB803": {
+                                    "@contents" : (
+                                        "METHOD:CANCEL",
+                                        ),
+                                },
+                            },
+                            "calendar": {
+                                ".db.sqlite": {
+                                    "@contents" : None, # ignore contents
+                                },
+                                "organizer.ics": {
+                                    "@contents" : (
+                                        "STATUS:CANCELLED",
+                                    ),
+                                },
+                                "attendee.ics": {
+                                    "@contents" : (
+                                        "ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED;SCHEDULE-STATUS=2.0:urn:uuid:\r\n 1CB4378B-DD76-462D-B4D4-BD131FE89243",
+                                        ),
+                                },
+                                "repeating_organizer.ics": {
+                                    "@contents" : (
+                                        "STATUS:CANCELLED",
+                                    ),
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        }
+        self.assertTrue(self.verifyHierarchy(
+            os.path.join(config.DocumentRoot, "calendars"),
+            after)
+        )
+
+
+
+    @inlineCallbacks
+    def test_purgeMultipleNonExistentGUIDs(self):
+
+        before = {
+            "calendars" : {
+                "__uids__" : {
+                    "76" : { # Non-existent
+                        "7F" : {
+                            "767F9EB0-8A58-4F61-8163-4BE0BB72B873" : {
+                                "calendar": {
+                                    "@xattrs" :
+                                    {
+                                        resourceAttr : collectionType,
+                                    },
+                                    "noninvite.ics": {
+                                        "@contents" : NON_INVITE_ICS_3,
+                                    },
+                                    "organizer.ics": {
+                                        "@contents" : ORGANIZER_ICS_3,
+                                    },
+                                    "attendee.ics": {
+                                        "@contents" : ATTENDEE_ICS_3,
+                                    },
+                                    "attendee2.ics": {
+                                        "@contents" : ATTENDEE_ICS_4,
+                                    },
+                                },
+                            },
+                        },
+                    },
+                    "42" : { # Non-existent
+                        "EB" : {
+                            "42EB074A-F859-4E8F-A4D0-7F0ADCB73D87" : {
+                                "calendar": {
+                                    "@xattrs" :
+                                    {
+                                        resourceAttr : collectionType,
+                                    },
+                                    "organizer.ics": {
+                                        "@contents" : ORGANIZER_ICS_3,
+                                    },
+                                    "attendee.ics": {
+                                        "@contents" : ATTENDEE_ICS_3,
+                                    },
+                                    "attendee2.ics": {
+                                        "@contents" : ATTENDEE_ICS_4,
+                                    },
+                                },
+                            },
+                        },
+                    },
+                    "29" : { # Existing
+                        "1C" : {
+                            "291C2C29-B663-4342-8EA1-A055E6A04D65" : {
+                                "calendar": {
+                                    "@xattrs" :
+                                    {
+                                        resourceAttr : collectionType,
+                                    },
+                                    "organizer.ics": {
+                                        "@contents" : ORGANIZER_ICS_3,
+                                    },
+                                    "attendee.ics": {
+                                        "@contents" : ATTENDEE_ICS_3,
+                                    },
+                                    "attendee2.ics": {
+                                        "@contents" : ATTENDEE_ICS_4,
+                                    },
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        }
+        self.createHierarchy(before, config.DocumentRoot)
+        count, assignments = (yield purgeGUID(
+            "767F9EB0-8A58-4F61-8163-4BE0BB72B873",
+            self.directory, self.rootResource))
+
+        self.assertEquals(count, 3)
+
+        after = {
+            "__uids__" : {
+                "76" : { # Non-existent
+                    "7F" : {
+                        "767F9EB0-8A58-4F61-8163-4BE0BB72B873" : {
+                            "calendar": {
+                                ".db.sqlite": {
+                                    "@contents" : None, # ignore contents
+                                },
+                                "noninvite.ics": { # event in the past
+                                    "@contents" : NON_INVITE_ICS_3,
+                                },
+                            },
+                        },
+                    },
+                },
+                "42" : { # Non-existent -- untouched
+                    "EB" : {
+                        "42EB074A-F859-4E8F-A4D0-7F0ADCB73D87" : {
+                            "calendar": {
+                                "@xattrs" :
+                                {
+                                    resourceAttr : collectionType,
+                                },
+                                "organizer.ics": {
+                                    "@contents" : ORGANIZER_ICS_3,
+                                },
+                                "attendee.ics": {
+                                    "@contents" : ATTENDEE_ICS_3,
+                                },
+                                "attendee2.ics": {
+                                    "@contents" : ATTENDEE_ICS_4,
+                                },
+                            },
+                        },
+                    },
+                },
+                "29" : {
+                    "1C" : {
+                        "291C2C29-B663-4342-8EA1-A055E6A04D65" : {
+                            "inbox": {
+                                ".db.sqlite": {
+                                    "@contents" : None, # ignore contents
+                                },
+                                "*.ics/UID:7ED97931-9A19-4596-9D4D-52B36D6AB803": {
+                                    "@contents" : (
+                                        "METHOD:CANCEL",
+                                        ),
+                                },
+                                "*.ics/UID:79F26B10-6ECE-465E-9478-53F2A9FCAFEE": {
+                                    "@contents" : (
+                                        "METHOD:REPLY",
+                                        "ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED:urn:uuid:767F9EB0-8A58-4F61-8\r\n 163-4BE0BB72B873",
+                                        ),
+                                },
+                            },
+                            "calendar": {
+                                ".db.sqlite": {
+                                    "@contents" : None, # ignore contents
+                                },
+                                "organizer.ics": {
+                                    # Purging non-existent organizer; has non-existent
+                                    # and existent attendees
+                                    "@contents" : (
+                                        "STATUS:CANCELLED",
+                                    ),
+                                },
+                                "attendee.ics": {
+                                    # (Note: implicit scheduler doesn't update this)
+                                    # Purging non-existent attendee; has non-existent
+                                    # organizer and existent attendee
+                                    "@contents" : ATTENDEE_ICS_3,
+                                },
+                                "attendee2.ics": {
+                                    # Purging non-existent attendee; has non-existent
+                                    # attendee and existent organizer
+                                    "@contents" : (
+                                        "ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED;SCHEDULE-STATUS=2.0:urn:uuid:\r\n 767F9EB0-8A58-4F61-8163-4BE0BB72B873",
+                                    )
+                                },
+                            },
+                        },
+                    },
+                },
+            },
+        }
+        self.assertTrue(self.verifyHierarchy(
+            os.path.join(config.DocumentRoot, "calendars"),
+            after)
+        )
+
+
 future = (datetime.utcnow() + timedelta(days=1)).strftime("%Y%m%dT%H%M%SZ")
 past = (datetime.utcnow() - timedelta(days=1)).strftime("%Y%m%dT%H%M%SZ")
 
+# For test_purgeExistingGUID
+
+# No organizer/attendee
 NON_INVITE_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
 BEGIN:VEVENT
@@ -543,6 +917,7 @@
 END:VCALENDAR
 """.replace("\n", "\r\n") % (past,)
 
+# Purging existing organizer; has existing attendee
 ORGANIZER_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
 BEGIN:VEVENT
@@ -557,6 +932,7 @@
 END:VCALENDAR
 """.replace("\n", "\r\n") % (future,)
 
+# Purging existing attendee; has existing organizer
 ATTENDEE_ICS = """BEGIN:VCALENDAR
 VERSION:2.0
 BEGIN:VEVENT
@@ -571,3 +947,141 @@
 END:VCALENDAR
 """.replace("\n", "\r\n") % (future,)
 
+
+# For test_purgeNonExistentGUID
+
+# No organizer/attendee, in the past
+NON_INVITE_PAST_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:151AFC76-6036-40EF-952B-97D1840760BF
+SUMMARY:Non Invitation
+DTSTART:%s
+DURATION:PT1H
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (past,)
+
+# No organizer/attendee, in the future
+NON_INVITE_FUTURE_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:251AFC76-6036-40EF-952B-97D1840760BF
+SUMMARY:Non Invitation
+DTSTART:%s
+DURATION:PT1H
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (future,)
+
+
+# Purging non-existent organizer; has existing attendee
+ORGANIZER_ICS_2 = """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:7ED97931-9A19-4596-9D4D-52B36D6AB803
+SUMMARY:Organizer
+DTSTART:%s
+DURATION:PT1H
+ORGANIZER:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (future,)
+
+# Purging non-existent attendee; has existing organizer
+ATTENDEE_ICS_2 = """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:1974603C-B2C0-4623-92A0-2436DEAB07EF
+SUMMARY:Attendee
+DTSTART:%s
+DURATION:PT1H
+ORGANIZER:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (future,)
+
+# Purging non-existent organizer; has existing attendee; repeating
+REPEATING_ORGANIZER_ICS = """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:8ED97931-9A19-4596-9D4D-52B36D6AB803
+SUMMARY:Repeating Organizer
+DTSTART:%s
+DURATION:PT1H
+RRULE:FREQ=DAILY;COUNT=400
+ORGANIZER:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:1CB4378B-DD76-462D-B4D4-BD131FE89243
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (past,)
+
+
+# For test_purgeMultipleNonExistentGUIDs
+
+# No organizer/attendee
+NON_INVITE_ICS_3 = """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:151AFC76-6036-40EF-952B-97D1840760BF
+SUMMARY:Non Invitation
+DTSTART:%s
+DURATION:PT1H
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (past,)
+
+# Purging non-existent organizer; has non-existent and existent attendees
+ORGANIZER_ICS_3 = """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:7ED97931-9A19-4596-9D4D-52B36D6AB803
+SUMMARY:Organizer
+DTSTART:%s
+DURATION:PT1H
+ORGANIZER:urn:uuid:767F9EB0-8A58-4F61-8163-4BE0BB72B873
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:767F9EB0-8A58-4F61-8163-4BE0BB72B873
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:42EB074A-F859-4E8F-A4D0-7F0ADCB73D87
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (future,)
+
+# Purging non-existent attendee; has non-existent organizer and existent attendee
+# (Note: Implicit scheduling doesn't update this at all for the existing attendee)
+ATTENDEE_ICS_3 = """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:1974603C-B2C0-4623-92A0-2436DEAB07EF
+SUMMARY:Attendee
+DTSTART:%s
+DURATION:PT1H
+ORGANIZER:urn:uuid:42EB074A-F859-4E8F-A4D0-7F0ADCB73D87
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:767F9EB0-8A58-4F61-8163-4BE0BB72B873
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:42EB074A-F859-4E8F-A4D0-7F0ADCB73D87
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (future,)
+
+# Purging non-existent attendee; has non-existent attendee and existent organizer
+ATTENDEE_ICS_4 = """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:79F26B10-6ECE-465E-9478-53F2A9FCAFEE
+SUMMARY:2 non-existent attendees
+DTSTART:%s
+DURATION:PT1H
+ORGANIZER:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:291C2C29-B663-4342-8EA1-A055E6A04D65
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:767F9EB0-8A58-4F61-8163-4BE0BB72B873
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:urn:uuid:42EB074A-F859-4E8F-A4D0-7F0ADCB73D87
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n") % (future,)
+

Added: CalendarServer/trunk/doc/calendarserver_purge_principals.8
===================================================================
--- CalendarServer/trunk/doc/calendarserver_purge_principals.8	                        (rev 0)
+++ CalendarServer/trunk/doc/calendarserver_purge_principals.8	2010-04-01 18:16:19 UTC (rev 5434)
@@ -0,0 +1,56 @@
+.\"
+.\" 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.
+.\"
+.\" The following requests are required for all man pages.
+.Dd June 17, 2009
+.Dt CALENDARSERVER_PURGE_PRINCIPALS 8
+.Os
+.Sh NAME
+.Nm calendarserver_purge_principals
+.Nd Darwin Calendar Server deprovisioned user clean-up utility
+.Sh SYNOPSIS
+.Nm
+.Op Fl -config Ar file
+.Op Fl -dry-run
+.Op Fl -verbose
+.Op Fl -help
+.Ar guid
+.Op guid ...
+.Sh DESCRIPTION
+.Nm
+is a tool for removing one or more principals' future events and proxy assignments from the calendar server.  Events in the past are retained, but any ongoing events are canceled.
+.Pp
+.Nm
+should be run as a user with the same priviledges as the Calendar
+Server itself, as it needs to read and write data that belongs to the
+server.
+.Sh OPTIONS
+.Bl -tag -width flag
+.It Fl h, -help
+Display usage information
+.It Fl f, -config Ar FILE
+Use the Calendar Server configuration specified in the given file.  Defaults to /etc/caldavd/caldavd.plist.
+.It Fl n, -dry-run
+Calculate and display how many events would be removed, but don't actually remove them.  
+.It Fl v, -verbose
+Print progress information.
+.El
+.Sh FILES
+.Bl -tag -width flag
+.It /etc/caldavd/caldavd.plist
+The Calendar Server configuration file.
+.El
+.Sh SEE ALSO
+.Xr caldavd 8

Modified: CalendarServer/trunk/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/aggregate.py	2010-04-01 17:48:04 UTC (rev 5433)
+++ CalendarServer/trunk/twistedcaldav/directory/aggregate.py	2010-04-01 18:16:19 UTC (rev 5434)
@@ -69,6 +69,15 @@
         self.realmName = realmName
         self._recordTypes = recordTypes
 
+        # FIXME: This is a temporary workaround until new data store is in
+        # place.  During the purging of deprovisioned users' data, we need
+        # to be able to look up records by guid and shortName.  The purge
+        # tool sticks temporary fake records in here.
+        self._tmpRecords = {
+            "guids" : { },
+            "shortNames" : { },
+        }
+
     def __repr__(self):
         return "<%s (%s): %r>" % (self.__class__.__name__, self.realmName, self._recordTypes)
 
@@ -111,9 +120,25 @@
             return records
 
     def recordWithShortName(self, recordType, shortName):
+
+        # FIXME: These temporary records shouldn't be needed when we move
+        # to the new data store API.  They're currently needed when purging
+        # deprovisioned users' data.
+        record = self._tmpRecords["shortNames"].get(shortName, None)
+        if record:
+            return record
+
         return self._query("recordWithShortName", recordType, shortName)
 
     def recordWithUID(self, uid):
+
+        # FIXME: These temporary records shouldn't be needed when we move
+        # to the new data store API.  They're currently needed when purging
+        # deprovisioned users' data.
+        record = self._tmpRecords["guids"].get(uid, None)
+        if record:
+            return record
+
         return self._queryAll("recordWithUID", uid)
 
     def recordWithAuthID(self, authID):

Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py	2010-04-01 17:48:04 UTC (rev 5433)
+++ CalendarServer/trunk/twistedcaldav/test/util.py	2010-04-01 18:16:19 UTC (rev 5434)
@@ -161,10 +161,10 @@
                     elif isinstance(expectedContents, tuple):
                         with open(childPath) as child:
                             contents = child.read()
-                            for str in expectedContents:
-                                if str not in contents:
+                            for term in expectedContents:
+                                if term not in contents:
                                     print "Contents mismatch:", childPath
-                                    print "Expecting match:\n%s\n\nActual:\n%s\n" % (str, contents)
+                                    print "Expecting match:\n%s\n\nActual:\n%s\n" % (term, contents)
                                     return False
                     else:
                         with open(childPath) as child:
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100401/936788c8/attachment-0001.html>


More information about the calendarserver-changes mailing list