[CalendarServer-changes] [5874] CalendarServer/branches/users/wsanchez/deployment

source_changes at macosforge.org source_changes at macosforge.org
Mon Jul 12 19:45:30 PDT 2010


Revision: 5874
          http://trac.macosforge.org/projects/calendarserver/changeset/5874
Author:   cdaboo at apple.com
Date:     2010-07-12 19:45:29 -0700 (Mon, 12 Jul 2010)
Log Message:
-----------
Tool to "purge" disabled users by cancelling/declining their invitations.

Modified Paths:
--------------
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py
    CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py

Added Paths:
-----------
    CalendarServer/branches/users/wsanchez/deployment/bin/calendarserver_purge_principals
    CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/purge.py

Added: CalendarServer/branches/users/wsanchez/deployment/bin/calendarserver_purge_principals
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/bin/calendarserver_purge_principals	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/bin/calendarserver_purge_principals	2010-07-13 02:45:29 UTC (rev 5874)
@@ -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()


Property changes on: CalendarServer/branches/users/wsanchez/deployment/bin/calendarserver_purge_principals
___________________________________________________________________
Added: svn:executable
   + *

Added: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/purge.py	                        (rev 0)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/purge.py	2010-07-13 02:45:29 UTC (rev 5874)
@@ -0,0 +1,414 @@
+#!/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.
+##
+
+from datetime import datetime
+from getopt import getopt, GetoptError
+from grp import getgrnam
+from pwd import getpwnam
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.util import switchUID
+from twistedcaldav.caldavxml import TimeRange
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.directory.directory import DirectoryError, DirectoryRecord
+from twistedcaldav.ical import Component, iCalendarProductID, Property
+from vobject.icalendar import utc
+import os
+import sys
+from calendarserver.tools.util import loadConfig, setupNotifications,\
+    getDirectory, setupMemcached
+from twistedcaldav.root import RootResource
+from twistedcaldav.static import CalendarHomeProvisioningFile
+from twistedcaldav import caldavxml
+from twisted.web2.http_headers import Headers
+
+class FakeRequest(object):
+
+    def __init__(self, rootResource, method, path):
+        self.rootResource = rootResource
+        self.method = method
+        self.path = path
+        self._resourcesByURL = {}
+        self._urlsByResource = {}
+        self.headers = Headers()
+
+    @inlineCallbacks
+    def _getChild(self, resource, segments):
+        if not segments:
+            returnValue(resource)
+
+        child, remaining = (yield resource.locateChild(self, segments))
+        returnValue((yield self._getChild(child, remaining)))
+
+    @inlineCallbacks
+    def locateResource(self, url):
+        url = url.strip("/")
+        segments = url.split("/")
+        resource = (yield self._getChild(self.rootResource, segments))
+        if resource:
+            self._rememberResource(resource, url)
+        returnValue(resource)
+
+    def _rememberResource(self, resource, url):
+        self._resourcesByURL[url] = resource
+        self._urlsByResource[resource] = url
+        return resource
+
+    def urlForResource(self, resource):
+        url = self._urlsByResource.get(resource, None)
+        if url is None:
+            class NoURLForResourceError(RuntimeError):
+                pass
+            raise NoURLForResourceError(resource)
+        return url
+
+    def addResponseFilter(*args, **kwds):
+        pass
+
+def usage_purge_principal(e=None):
+
+    if e:
+        print "Error: %s" % (e,)
+        print ""
+    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:
+            config.directory = getDirectory()
+
+            #
+            # Wire up the resource hierarchy
+            #
+            principalCollection = config.directory.getPrincipalCollection()
+            rootResource = RootResource(
+                config.DocumentRoot,
+                principalCollections=(principalCollection,),
+            )
+            rootResource.putChild("principals", principalCollection)
+            calendarCollection = CalendarHomeProvisioningFile(
+                os.path.join(config.DocumentRoot, "calendars"),
+                config.directory, "/calendars/",
+            )
+            rootResource.putChild("calendars", calendarCollection)
+
+        except DirectoryError, e:
+            print "Error: %s" % (e,)
+            return
+        setupMemcached(config)
+        setupNotifications(config)
+    except ConfigurationError, e:
+        raise
+
+
+    #
+    # Start the reactor
+    #
+    reactor.callLater(0.1, callThenStop, method, config.directory,
+        rootResource, *args, **kwds)
+
+    reactor.run()
+
+def main_purge_principals():
+
+    try:
+        (optargs, args) = getopt(
+            sys.argv[1:], "f:hnv", [
+                "dry-run",
+                "config=",
+                "help",
+                "verbose",
+            ],
+        )
+    except GetoptError, e:
+        usage_purge_principal(e)
+
+    #
+    # Get configuration
+    #
+    configFileName = None
+    dryrun = False
+    verbose = False
+
+    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
+
+    try:
+        shared_main(configFileName, purgeGUIDs, args, verbose=verbose, dryrun=dryrun)
+    except ConfigurationError, e:
+        usage_purge_principal(e)
+
+ at inlineCallbacks
+def callThenStop(method, *args, **kwds):
+    try:
+        count = (yield method(*args, **kwds))
+        if kwds.get("dryrun", False):
+            print "Would have purged %d principals" % (count,)
+        else:
+            print "Purged %d principals" % (count,)
+    except Exception, e:
+        print "Error: %s" % (e,)
+    finally:
+        reactor.stop()
+
+
+ at inlineCallbacks
+def purgeGUIDs(directory, root, guids, verbose=False, dryrun=False):
+    total = 0
+
+    for guid in guids:
+        yield purgeGUID(guid, directory, root, verbose=verbose, dryrun=dryrun)
+        total += 1
+
+    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: probably 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 is left alone
+    now = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
+    filter =  caldavxml.Filter(
+          caldavxml.ComponentFilter(
+              caldavxml.ComponentFilter(
+                  TimeRange(start=now,),
+                  name=("VEVENT",),
+              ),
+              name="VCALENDAR",
+           )
+      )
+
+    organized = 0
+    attended = 0
+
+    for collName in calendarHome.listChildren():
+        collection = calendarHome.getChild(collName)
+        if collection.isCalendarCollection():
+
+            for name, _ignore_uid, type in collection.index().search(filter):
+                if isinstance(name, unicode):
+                    name = name.encode("utf-8")
+                resource = collection.getChild(name)
+                uri = "/calendars/__uids__/%s/%s/%s" % (
+                    record.guid,
+                    collName,
+                    name
+                )
+                if not dryrun:
+                    result = (yield processResource(root, collection, resource,
+                                                    uri, guid, principal, implicit=True))
+                    if result is not None:
+                        if result:
+                            organized += 1
+                        else:
+                            attended += 1
+
+    print "Cancelled %d events, declined %d events, for user: %s" % (organized, attended, guid,)
+
+ at inlineCallbacks
+def processResource(root, collection, resource, uri, guid, principal, implicit=False):
+    
+    calendar = resource.iCalendar()
+    organizer = calendar.getOrganizer()
+    if organizer is None:
+        returnValue(None)
+    organizerPrincipal = resource.principalForCalendarUserAddress(organizer)
+
+    # Determine if user is the Organizer
+    isOrganizer = guid == organizerPrincipal.principalUID()
+    
+    if not isOrganizer:
+        cuas = principal.calendarUserAddresses()
+        attendeeProps = calendar.getAttendeeProperties(cuas)
+        if not attendeeProps:
+            returnValue(None)
+
+    originator = None
+    recipients = None
+    if isOrganizer:
+        originator = organizer
+        allAttendees = tuple(getAllAttendeeProperties(calendar))
+        recipients = [attendee.value() for attendee in allAttendees if attendee.value() != organizer]
+
+        # Send iTIP CANCEL to all ATTENDEEs
+        master = calendar.masterComponent()
+        if master is None:
+            for component in calendar.subcomponents():
+                if component.name() != "VTIMEZONE":
+                    master = component
+                    break
+        
+        itipmsg = Component("VCALENDAR")
+        itipmsg.addProperty(Property("PRODID", iCalendarProductID))
+        itipmsg.addProperty(Property("METHOD", "CANCEL"))
+        itipmsg.addProperty(Property("VERSION", "2.0"))
+        event = Component("VEVENT")
+        itipmsg.addComponent(event)
+        event.addProperty(Property("DTSTAMP", datetime.now(tz=utc)))
+        event.addProperty(Property("UID", calendar.resourceUID()))
+        if master.hasProperty("SUMMARY"):
+            event.addProperty(Property("SUMMARY", master.propertyValue("SUMMARY")))
+        if master.hasProperty("DTSTART"):
+            event.addProperty(master.getProperty("DTSTART"))
+        if master.hasProperty("DTEND"):
+            event.addProperty(master.getProperty("DTEND"))
+        if master.hasProperty("DURATION"):
+            event.addProperty(master.getProperty("DURATION"))
+        event.addProperty(master.getProperty("ORGANIZER"))
+        for attendee in allAttendees:
+            event.addProperty(attendee)
+            
+        # Bump sequence
+        if master.hasProperty("SEQUENCE"):
+            sequence = int(master.propertyValue("SEQUENCE"))
+        else:
+            sequence = 0
+        event.addProperty(Property("SEQUENCE", str(sequence + 1)))
+
+    else:
+        # Send iTIP DECLINE to ORGANIZER
+        itipmsg = calendar.duplicate()
+        itipmsg.addProperty(Property("METHOD", "REPLY"))
+        itipmsg.getProperty("PRODID").setValue(iCalendarProductID)
+        
+        # Add REQUEST-STATUS
+        for component in itipmsg.subcomponents():
+            component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
+
+        # Remove all attendees other than ourselves
+        for component in itipmsg.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            attendeeProp = component.getAttendeeProperty(cuas)
+            attendees = tuple(component.properties("ATTENDEE"))
+            for attendee in attendees:
+                if attendeeProp is None or (attendee.value() != attendeeProp.value()):
+                    component.removeProperty(attendee)
+        
+        # Decline all (and check if they were all declined before)
+        allDeclined = True
+        attendeeProps = itipmsg.getAttendeeProperties(cuas)
+        for attendeeProp in attendeeProps:
+            if "PARTSTAT" in attendeeProp.params():
+                if attendeeProp.params()["PARTSTAT"][0] != "DECLINED":
+                    allDeclined = False
+                attendeeProp.params()["PARTSTAT"][0] = "DECLINED"
+            else:
+                attendeeProp.params()["PARTSTAT"] = ["DECLINED"]
+                allDeclined = False
+
+        originator = attendeeProps[0].value()
+        recipients = [organizer,]
+        
+        if allDeclined:
+            returnValue(None)
+        
+    # Send iTIP message
+    # This is a local CALDAV scheduling operation.
+    from twistedcaldav.scheduling.scheduler import CalDAVScheduler
+    scheduler = CalDAVScheduler(FakeRequest(root, "PUT", uri), resource)
+
+    # Do the POST processing treating
+    yield scheduler.doSchedulingViaPUT(originator, recipients, itipmsg, True)
+
+    returnValue(isOrganizer)
+
+# ical.py extensions that are already on trunk but not being back-ported to deployment right now
+
+def getAllAttendeeProperties(self):
+    """
+    Yield all attendees as Property objects.  Works on either a VCALENDAR or
+    on a component.
+    @return: a generator yielding Property objects
+    """
+
+    # Extract appropriate sub-component if this is a VCALENDAR
+    if self.name() == "VCALENDAR":
+        for component in self.subcomponents():
+            if component.name() != "VTIMEZONE":
+                for attendee in getAllAttendeeProperties(component):
+                    yield attendee
+    else:
+        # Find the primary subcomponent
+        for attendee in self.properties("ATTENDEE"):
+            yield attendee
+
+if __name__ == "__main__":
+    main_purge_principals()
+


Property changes on: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/purge.py
___________________________________________________________________
Added: svn:executable
   + *

Modified: CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py	2010-07-12 23:12:58 UTC (rev 5873)
+++ CalendarServer/branches/users/wsanchez/deployment/calendarserver/tools/util.py	2010-07-13 02:45:29 UTC (rev 5874)
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from twistedcaldav.notify import installNotificationClient
+from twistedcaldav import memcachepool
 
 __all__ = [
     "loadConfig",
@@ -135,3 +137,24 @@
 
     except socket.error:
         config.Memcached.Pools.Default.ClientEnabled = False
+
+def setupMemcached(config):
+    #
+    # Connect to memcached
+    #
+    memcachepool.installPools(
+        config.Memcached.Pools,
+        config.Memcached.MaxClients
+    )
+    autoDisableMemcached(config)
+
+def setupNotifications(config):
+    #
+    # Connect to notifications
+    #
+    if config.Notifications.Enabled:
+        installNotificationClient(
+            config.Notifications.InternalNotificationHost,
+            config.Notifications.InternalNotificationPort,
+        )
+

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/put_common.py	2010-07-12 23:12:58 UTC (rev 5873)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/method/put_common.py	2010-07-13 02:45:29 UTC (rev 5874)
@@ -465,7 +465,7 @@
             # property cache before we do this as the etag property may have changed.
             #
             # Currently only care about this on a PUT
-            if source is None:
+            if source is None and not isiTIP:
                 if destination.exists():
                     destination.deadProperties().reloadCache()
                 destination.checkPreconditions(request)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py	2010-07-12 23:12:58 UTC (rev 5873)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/caldav.py	2010-07-13 02:45:29 UTC (rev 5874)
@@ -153,7 +153,7 @@
                 isiTIP = True
             )
         except: # FIXME: bare except
-            self.log_error("Could not store data in Inbox : %s" % (recipient.inbox,))
+            log.error("Could not store data in Inbox : %s" % (recipient.inbox,))
             err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
             responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
             returnValue(False)

Modified: CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py	2010-07-12 23:12:58 UTC (rev 5873)
+++ CalendarServer/branches/users/wsanchez/deployment/twistedcaldav/scheduling/scheduler.py	2010-07-13 02:45:29 UTC (rev 5874)
@@ -117,6 +117,7 @@
         self.originator = originator
         self.recipients = recipients
         self.calendar = calendar
+        self.calendardata = str(calendar)
         self.internal_request = internal_request
 
         # Do some extra authorization checks
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100712/f9d28eda/attachment-0001.html>


More information about the calendarserver-changes mailing list